NeHe - Lektion 27 - Schatten

Lektion 27



Willkommen zu einem ziemlich komplexen Tutorial über Schatten-werfen. Der Effekt, den dieses Demo erzeugt ist einfach unglaublich. Schatten die langgezogen, gekrümmt und gebrochen an anderen Objekten oder entlägs von Mauern verlaufen. Alles in der Szene kann mittels der Tasten auf der Tastatur im 3D Raum bewegt werden.

Dieses Tutorial wählt diesmal einen etwas anderen Ansatz - es wird davon ausgegangen, dass Sie bereits gute OpenGL Kentnisse haben. Sie sollten den Stencil Buffer verstehen und das grundlegende OpenGL Setup. Wenn Sie eine Auffrischung benötigen, gehen Sie zurück und lesen Sie die vorherigen Tutorials. Funktionen wie CreateGLWindow und WinMain werden NICHT in diesem Tutorial erklärt. Außerdem wird fundamentale 3D Mathematik vorausgesetzt, halten Sie also ein Nachschlagewerk bereit! (Ich habe meine Aufzeichnungen des ersten Semesters aus der Uni genommen - Ich wußte, dass dieser mal später nützlich sein werden! :)

Als erstes haben wir die Defintion von INFINITY (=unendlich), welche beschreibt, wie weit sich die "Schatten-Volumen-Polygone" ausbreiten (das wird später noch erklärt). Wenn Sie ein größeres oder kleineres Koordinaten-System verwenden, passen Sie diesen Wert entsprechend an.

// Definition von "INFINITY" um den Ausbreitungs-Vektor für das Schatten-Volumen zu berechnen
#define INFINITY    100

Als nächstes folgt die Definition der Objekt-Strukturen.

Die Point3f Struktur enthält eine Koordinate im 3D Raum. Diese kann für Vertices oder Vekotren verwendet werden.

// Struktur die ein Vertex in einem Objekt beschreibt
struct Point3f
{
    GLfloat x, y, z;
};

Die Plane-Struktur enthält 4 Werte, die die Gleichung einer Ebene darstellen. Diese Ebenen werden die Seiten eines Objektes darstellen.

// Struktur die eine Ebene beschreibt, in dem Format: ax + by + cz + d = 0
struct Plane
{
    GLfloat a, b, c, d;
};

Die Face Struktur enthält alle Informationen, die über eine Dreieck benötigt werden, um einen Schatten zu werfen.
  • Die angegebenen Indize kommen aus dem Vertice-Array des Objektes.
  • Die Vertex Normalenvektoren werden benutzt, um die Orientierung der Seite im 3D Raum zu berechnen, so dass Sie bestimmen können, welche der Lichtquelle zugewandt ist, wenn Sie einen Schatten werfen.
  • Die Ebenen-Gleichung beschreibt die Ebene, in der dieses Dreieck liegt, im 3D Raum.
  • Die Nachbars-Indize sind Indize in das Array von Seiten des Objektes. Das erlaubt Ihnen anzugeben, welche Seite mit dieser Seite an jeder Ecke des Dreiecks verbunden werden soll.
  • Der visible Parameter wird benutzt, um anzugeben, ob die Seite "sichtbar" für die Lichtquelle ist, welche die Schatten wirft.


// Struktur die eine Objekt-Seite beschreibt
struct Face
{
    int vertexIndices[3];            // Index eines jeden Vertex innerhalb eines Objekts, dass das Dreieck dieser Seite bildet
    Point3f normals[3];            // Normalenvektor für jeden Vertex
    Plane planeEquation;            // Gleichung einer Ebene, in der das Dreieck liegt
    int neighbourIndices[3];        // Index einer jeden Seite, die mit diesem in dem Objekt benachbart ist
    bool visible;                // Ist die Seite für das Licht sichtbar?
};

Letzlich enthält die ShadowedObject Struktur all die Vertices und Seiten aus dem Objekt. Der Speicher für jedes Array wird dynamisch erzeugt, wenn sie geladen wird.

struct ShadowedObject
{
    int nVertices;
    Point3f *pVertices;            // wird dynamisch alloziiert

    int nFaces;
    Face *pFaces;                // wird dynamisch alloziiert
};

Die readObject Funktion ist ziemlich selbsterklärend. Sie füllt die gegebenen Strukturen mit Werten, die aus einer Datei gelesen werden, alloziiert Speicher für die Vertices und für die Seiten. Sie initialisiert außerdem die Nachbarn auf -1, was bedeutet, dass es (bis jetzt) keine gibt. Diese werden später berechnet.

bool readObject( const char *filename, ShadowedObject& object )
{
    FILE *pInputFile;
    int i;

    pInputFile = fopen( filename, "r" );
    if ( pInputFile == NULL )
    {
        cerr // lese Vertices
    fscanf( pInputFile, "%d", &object.nVertices );
    object.pVertices = new Point3f[object.nVertices];
    for ( i = 0; i <object.nVertices; i++ )
    {
        fscanf( pInputFile, "%f", &object.pVertices[i].x );
        fscanf( pInputFile, "%f", &object.pVertices[i].y );
        fscanf( pInputFile, "%f", &object.pVertices[i].z );
    }

    // lese Seiten
    fscanf( pInputFile, "%d", &object.nFaces );
    object.pFaces = new Face[object.nFaces];
    for ( i = 0; i <object.nFaces; i++ )
    {
        int j;
        Face *pFace = &object.pFaces[i];

        for ( j = 0; j <3; j++ )
            pFace->neighbourIndices[j] = -1;    // es gibt noch keine Nachbarn

        for ( j = 0; j <3; j++ )
        {
            fscanf( pInputFile, "%d", &pFace->vertexIndices[j] );
            pFace->vertexIndices[j]--;        // Dateien geben Sie in einem Array basierend auf 1 an, aber wir benutzen ein Array das mit 0 beginnt
        }

        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;
}

Genauso ist killObjekt selbsterklärend - es werden einfach alle dynamisch alloziierten Arrays innerhalb des Objekts gelöscht, wenn Sie mit diesen fertig sind. Beachten Sie, dass eine Zeile zu KillGLWindow hinzugefügt wurde, um diese Funktion für die betreffenden Objekte aufzurufen.

void killObject( ShadowedObject& object )
{
    delete[] object.pFaces;
    object.pFaces = NULL;
    object.nFaces = 0;

    delete[] object.pVertices;
    object.pVertices = NULL;
    object.nVertices = 0;
}

Nun, mit setConnectivity wird es interessant. Diese Funktion wird verwendet, um herauszufinden, welche Nachbarn jede Seite in dem gegebenen Objekt hat. Hier etwas Pseudo-Code:

für jede Seite (A) in dem Objekt
        für jede Kante in A
                wenn wir den Kantennachbar noch nicht kennen
                        für jede Seite (B) in dem Objekt (außer A)
                                für jede Kante in B
                                        wenn A's Kante die selbe wie B's Kante ist, dann sind diese über diese Kante benachbart
                                                setze die Nachbars-Eigenschaft für jede Seite A und B, dann fahre mit der nächsten Kante in A fort

Die letzten beiden Zeilen werden mit folgendem Code verwirklicht. Indem die zwei Vertices gefunden werden, die das Ende der Kante markieren und diese verglichen werden, können Sie feststellen, ob es die selbe Kante ist. Der Teil (edgeA+1)%3 ermittelt einen Vertex, der neben dem liegt, den Sie betrachten. Dann überprüfen Sie, ob die Vertices übereinstimmen (die Reihenfolge mag differieren, abhängig vom zweiten Fall des if Statements).

    int vertA1 = pFaceA->vertexIndices[edgeA];
    int vertA2 = pFaceA->vertexIndices[( edgeA+1 )%3];

    int vertB1 = pFaceB->vertexIndices[edgeB];
    int vertB2 = pFaceB->vertexIndices[( edgeB+1 )%3];

    // überprüfe ob es Nachbarn gibt - z.B. Die Kanten sind die Selben
    if (( vertA1 == vertB1 && vertA2 == vertB2 ) || ( vertA1 == vertB2 && vertA2 == vertB1 ))
    {
        pFaceA->neighbourIndices[edgeA] = faceB;
        pFaceB->neighbourIndices[edgeB] = faceA;
        edgeFound = true;
        break;
    }

Glücklicherweise eine andere einfach Funktion, während Sie erst mal Luft holen können. drawObject rendert jede Seite, eine nach der anderen.

// Zeichne ein Objekt - Zeichne einfach jede Dreiecks-Seite.
void drawObject( const ShadowedObject& object )
{
    glBegin( GL_TRIANGLES );
    for ( int i = 0; i <object.nFaces; i++ )
    {
        const Face& face = object.pFaces[i];

        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();
}

Die Berechnung der Ebenen-Gleichung sieht ziemlich hässlich aus, aber es ist nur eine einfache mathematische Formel, die Sie in jedem Mathebuch nachschlagen können, wenn Sie sie benötigen.

void calculatePlane( const ShadowedObject& object, Face& face )
{
    // ermittle verkürzte Namen für die Vertices für jede Seite
    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) );
}

Haben Sie bereits einmal tief Luft geholt? Gut, da Sie jetzt lernen werden, wie man einen Schatten wirft! Die castShadow Funktion enthält all den GL spezifischen Kram und übergibt diesen doShadowPass, um den Schatten in zwei Durchgängen zu rendern.

Als erstes bestimmen wir, welche Fläche zum Licht gerichtet ist. Wir machen das, indem wir nachschauen, auf welche Ebene das Licht scheint. Das wird dadurch erreicht, dass wir die Lichtposition in die Ebene für die Gleichung einsetzen. Wenn diese größer als 0 ist, dann ist diese in der selben Richtung wie der Normalenvektor zur Ebene und sichbar für das Licht. Wenn nicht, dann ist sie nicht sichtbar für das Licht (Erneut: schauen Sie in einem guten Mathebuch für 3D-Geometrie nach, für eine bessere Erklärung).

void castShadow( ShadowedObject& object, GLfloat *lightPosition )
{
    // bestimme, welche Seiten für das Licht sichtbar sind.
    for ( int i = 0; i <object.nFaces; i++ )
    {
        const Plane& plane = object.pFaces[i].planeEquation;

        GLfloat side = plane.a*lightPosition[0]+
            plane.b*lightPosition[1]+
            plane.c*lightPosition[2]+
            plane.d;

        if ( side > 0 )
            object.pFaces[i].visible = true;
        else
            object.pFaces[i].visible = false;
    }

Der nächste Abschnitt setzt die nötigen OpenGL Status um Schatten zu rendern.

Als erstes pushen wir alle Attribute auf den Stack, die modifiziert werden. Das macht das Wiederherstellen um ein vielfaches leichter.

Beleuchtung ist deaktiviert, da wir nicht in den Farb (Ausgabe) Buffer rendern werden, nur in den Stencil Buffer. Aus dem selben Grund schaltet die Farb-Maske alle Farb-Komponenten aus (weshalb das Zeichnen eines Polygons nicht bis zum Ausgabe Buffer gelangen würde).

Obwohl Depth Testing immer noch benutzt wird, wollen wir die Schatten nicht als solide Objekte im Depth Buffer erscheinen lassen, weshalb die Depth Maske das verhindert.

Der Stencil Buffer wird eingeschaltet, weil in diesen die Schatten gezeichnet werden.

    glPushAttrib( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ENABLE_BIT | GL_POLYGON_BIT | GL_STENCIL_BUFFER_BIT );
    glDisable( GL_LIGHTING );                    // schalte Beleuchtung aus
    glDepthMask( GL_FALSE );                    // schalte Schreibzugriffe auf den Depth-Buffer aus
    glDepthFunc( GL_LEQUAL );
    glEnable( GL_STENCIL_TEST );                    // schalte Stencil Buffer Testing ein
    glColorMask( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE );        // zeichne nicht in den Colour Buffer
    glStencilFunc( GL_ALWAYS, 1, 0xFFFFFFFFL );

Ok, nun werden die Schatten tatsächlich gerendert. Wir kommen darauf gleich zurück, wenn wir uns die doShadowPass Funktion anschauen. Diese werden in zwei Durchgängen gerendert, wie Sie sehen können, einer inkrementiert den Stencil Buffer mit der Vorder-Seite (welche den Schatten wirft), der zweite dekrementiert den Stencil Buffer mit der Hinter-Seite ("ausschalten" des Schattens zwischen dem Objekt und jeder anderen Oberfläche).

    // Erster Durchgang. Inkrementiere Stencil Wert im Schatten.
    glFrontFace( GL_CCW );
    glStencilOp( GL_KEEP, GL_KEEP, GL_INCR );
    doShadowPass( object, lightPosition );
    // Zweiter Durchgang. Dekrementiere Stencil Wert im Schatten,
    glFrontFace( GL_CW );
    glStencilOp( GL_KEEP, GL_KEEP, GL_DECR );
    doShadowPass( object, lightPosition );

Um den zweiten Durchgang zu verstehen, kommentiere Sie diesen am besten aus und schauen, wie sich der Code dann verhält. Um Ihnen die Arbeit zu ersparen, habe ich diese Arbeit für Sie gemacht:

Abbildung 1: Erster Durchgang Abbildung 2: Zweiter Durchgang


Der letzte Abschnitt dieser Funktion zeichnet ein geblendetes Rechteck über den gesamten Bildschirm, um den Schatten zu werfen. Umso dunkler Sie das Rechteck machen, umso dunkler wird der Schatten sein. Um also die Eigenschaften des Schattens zu ändern, ändern Sie das glColor4f Statement. Ein höherer Alpha-Wert wird ihn dunkle machen. Oder Sie können ihn rot, grün, purpur,... machen!

    glFrontFace( GL_CCW );
    glColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE );    // aktiviere Rendering in den Colour Buffer für alle Komponenten

    // Zeichne ein schattiges Rechteck über den gesamten Bildschirm
    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();
}

Ok, der nächste Teil zeichnet die schattierten Quads. Wie funktioniert das? Was passiert ist, dass Sie durch jede Seite iterieren und wenn diese sichtbar ist, dann überprüfen Sie alle ihre Kanten. Wenn es an der Kante keine benachbarten Seiten gibt oder die benachbarten Seiten nicht sichtbar sind, wirft die Kante einen Schatten. Wenn Sie über die beiden Fälle genau nachdenken, werden Sie bemerken, dass es stimmt. Indem Sie einen Viereck (bestehend aus zwei Dreiecken) zeichnen, bestehend aus den Punkten der Kante und die Kante wird nach hinten durch die Szene projiziert, um den Schattenwurf zu erhalten.

Die "Brute Force" Annäherung, die hier genutzt wird, zeichnet einfach bis "unendlich" und die Schattenpolygone werden einfach an den Polygonen geclippt, auf die sie treffen. Das zieht ein 'Durchdringen' mit sich, was allerdings die Video-Hardware belastet. Für einen High-Performance Änderung an diesem Algorithmus sollten Sie die Polygone an den Objekten dahinter clippen. Das ist viel trickreicher und bringt ein paar Probleme mit sich, aber wenn Sie es so machen wollen, sollten Sie sich diesen Gamasutra Artikel anschauen.

Der Code, der all das macht ist nicht so trickreich, wie es sich anhört. Wir fangen mit einem Ausschnitt an, der durch die Objekte iteriert. Am Ende haben wir eine Kante j und seine benachbarten Seiten, spezifiziert durch neighbourIndex.

void doShadowPass( ShadowedObject& object, GLfloat *lightPosition )
{
    for ( int i = 0; i <object.nFaces; i++ )
    {
        const Face& face = object.pFaces[i];

        if ( face.visible )
        {
            // Go Through Each Edge
            for ( int j = 0; j <3; j++ )
            {
                int neighbourIndex = face.neighbourIndices[j];

Als nächstes überprüfen Sie, ob es eine sichtbare benachbarte Seite dieses Objektes gibt. Wenn nicht, dann wirft diese Kante einen Schatten.

                // wenn es keinen Nachbarn gibt oder die benachbarte Seite nicht sichtbar ist, dann wirft diese Kante einen Schatten
                if ( neighbourIndex == -1 || object.pFaces[neighbourIndex].visible == false )
                {

Der nächste Codeabschnitt ermittelt die zwei Vertices der aktuellen Kante, v1 und v2. Dann berechnet sie v3 und v4, welche entlängs des Vektors zwischen der Lichtquelle und der ersten Kante projiziert wird. Sie werden auf UNENDLICH (INFINITY) skaliert, was auf einen ziemlich großen Wert gesetzt wurde.

                    // ermittle die Punkte auf der Kante
                    const Point3f& v1 = object.pVertices[face.vertexIndices[j]];
                    const Point3f& v2 = object.pVertices[face.vertexIndices[( j+1 )%3]];

                    // berechne den Abstand der beiden Vertices
                    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;

Ich denke Sie verstehen den nächsten Abschnitt, er zeichnet einfach das Viereck, welches durch diese vier Punkte definiert ist:

                    // zeichne das Viereck (als Triangle Strip)
                    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();
                }
            }
        }
    }
}

Damit ist der Schattenwerfen-Abschnitt komplett. Aber wir sind noch nicht fertig! Was ist mit der drawGLScene? Fangen wir mit den einfachen Teilen an: die Buffer löschen, die Lichtquelle positionieren und eine Sphere zeichnen:

bool drawGLScene()
{
    GLmatrix16f Minv;
    GLvector4f wlp, lp;

    // lösche Color Buffer, Depth Buffer, Stencil Buffer
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

    glLoadIdentity();                        // Resette Modelview Matrix
    glTranslatef(0.0f, 0.0f, -20.0f);                // Zoom 20 Einheiten in den Bildschirm hinein
    glLightfv(GL_LIGHT1, GL_POSITION, LightPos);            // Positioniere Light1
    glTranslatef(SpherePos[0], SpherePos[1], SpherePos[2]);        // Positioniere die Sphere
    gluSphere(q, 1.5f, 32, 16);                    // zeichne eine Sphere

Als nächstes müssen wir die Lichtquelle relativ zum lokalen Koordinatensystem des Objektes berechnen. Die Kommentare erklären jeden Schritt detailiert. Minv speichert die Objekt Transformations Matrix, allerdings in umgekehrter Reihenfolge und mit negativen Argumenten, weshalb es eigentlich die Inverse der Transformations Matrix ist. Dann wird lp als Kopie der Lichtposition erzeugt und mit der Matrix multipliziert. Deshalb ist lp die Lichtposition im Koordinatensystem des Objekts.

    glLoadIdentity();                        // Resette Matrix
    glRotatef(-yrot, 0.0f, 1.0f, 0.0f);                // Rotiere um -yrot auf der Y-Achse
    glRotatef(-xrot, 1.0f, 0.0f, 0.0f);                // Rotiere um -xrot auf der X-Achse
    glTranslatef(-ObjPos[0], -ObjPos[1], -ObjPos[2]);        // bewege negativ auf allen Achsen basierend auf den ObjPos[] Werten (X, Y, Z)
    glGetFloatv(GL_MODELVIEW_MATRIX,Minv);                // ermittle ModelView Matrix (speichere in Minv)
    lp[0] = LightPos[0];                        // speichere Lichtposition X in lp[0]
    lp[1] = LightPos[1];                        // speichere Lichtposition Y in lp[1]
    lp[2] = LightPos[2];                        // speichere Lichtposition Z in lp[2]
    lp[3] = LightPos[3];                        // speichere Licht-Richtung in lp[3]
    VMatMult(Minv, lp);                        // wir speichern den rotierten Lichtvektor im 'lp' Array

Nun, etwas Arbeit um den Raum und das Objekt zu zeichnen. Der Aufruf von castShadow zeichnet die Schatten des Objekts.

    glLoadIdentity();                        // Resette die Modelview Matrix
    glTranslatef(0.0f, 0.0f, -20.0f);                // Zoome 20 Einheiten in den Bildschirm hinein
    DrawGLRoom();                            // zeichne den Raum
    glTranslatef(ObjPos[0], ObjPos[1], ObjPos[2]);            // Positioniere das Objekt
    glRotatef(xrot, 1.0f, 0.0f, 0.0f);                // rotiere auf der X-AChse um xrot
    glRotatef(yrot, 0.0f, 1.0f, 0.0f);                // rotiere auf der Y-Achse um yrot
    drawObject(obj);                        // Prozedur um das geladene Objekt zu zeichnen
    castShadow(obj, lp);                        // Prozedur um den Schatten basierend auf der Silhouette zu werfen

Die folgende paar Zeilen zeichnen einen kleinen orangenen Kreis, da wo das Licht ist:

    glColor4f(0.7f, 0.4f, 0.0f, 1.0f);                // Setze Farbe auf ein Orange
    glDisable(GL_LIGHTING);                        // deaktiviere Beleuchtung
    glDepthMask(GL_FALSE);                        // deaktiviere Depth Mask
    glTranslatef(lp[0], lp[1], lp[2]);                // Translatiere zur Licht-Position
                                    // Beachten Sie, wir sind immer noch im lokalen Koordinaten-System
    gluSphere(q, 0.2f, 16, 8);                    // zeichne eine kleine gelbe Sphere (repräsentiert das Licht)
    glEnable(GL_LIGHTING);                        // aktiviere Beleuchtung
    glDepthMask(GL_TRUE);                        // aktiviere Depth Mask

Der letzte Teil aktuallisiert die Objekt Position und kehrt zurück.

    xrot += xspeed;                            // inkrementiere xrot um  xspeed
    yrot += yspeed;                            // inkrementiere yrot um yspeed

    glFlush();                            // Flushe die OpenGL Pipeline
    return TRUE;                            // alles verlief OK
}

Wir haben eine DrawGLRoom Funktion spezifiziert und hier ist sie - viele Rechtecke, auf die Schatten geworfen werden:

void DrawGLRoom()                            // Zeichne den Raum (Box)
{
    glBegin(GL_QUADS);                        // fange an Quads zu zeichnen
        // Boden
        glNormal3f(0.0f, 1.0f, 0.0f);                // Normalenvektor der nach oben zeigt
        glVertex3f(-10.0f,-10.0f,-20.0f);            // hinten links
        glVertex3f(-10.0f,-10.0f, 20.0f);            // vorne links
        glVertex3f( 10.0f,-10.0f, 20.0f);            // vorne rechts
        glVertex3f( 10.0f,-10.0f,-20.0f);            // hinten rechts
        // Decke
        glNormal3f(0.0f,-1.0f, 0.0f);                // Normalenvektor der nach unten zeigt
        glVertex3f(-10.0f, 10.0f, 20.0f);            // vorne links
        glVertex3f(-10.0f, 10.0f,-20.0f);            // hinten links
        glVertex3f( 10.0f, 10.0f,-20.0f);            // hinten rechts
        glVertex3f( 10.0f, 10.0f, 20.0f);            // vorne rechts
        // vordere Wand
        glNormal3f(0.0f, 0.0f, 1.0f);                // Normalenvektor der weg vom Betrachter zeigt
        glVertex3f(-10.0f, 10.0f,-20.0f);            // oben links
        glVertex3f(-10.0f,-10.0f,-20.0f);            // unten links
        glVertex3f( 10.0f,-10.0f,-20.0f);            // unten rechts
        glVertex3f( 10.0f, 10.0f,-20.0f);            // oben rechts
        // hintere Wand
        glNormal3f(0.0f, 0.0f,-1.0f);                // Normalenvektor der zum Betrachter zeigt
        glVertex3f( 10.0f, 10.0f, 20.0f);            // oben rechts
        glVertex3f( 10.0f,-10.0f, 20.0f);            // unten rechts
        glVertex3f(-10.0f,-10.0f, 20.0f);            // unten links
        glVertex3f(-10.0f, 10.0f, 20.0f);            // oben links
        // linke Wand
        glNormal3f(1.0f, 0.0f, 0.0f);                // Normalenvektor der nach rechts zeigt
        glVertex3f(-10.0f, 10.0f, 20.0f);            // oben vorne
        glVertex3f(-10.0f,-10.0f, 20.0f);            // unten vorne
        glVertex3f(-10.0f,-10.0f,-20.0f);            // unten hinten
        glVertex3f(-10.0f, 10.0f,-20.0f);            // oben hinten
        // rechte Wand
        glNormal3f(-1.0f, 0.0f, 0.0f);                // Normalenvektor der nach links zeigt
        glVertex3f( 10.0f, 10.0f,-20.0f);            // oben hinten
        glVertex3f( 10.0f,-10.0f,-20.0f);            // unten hinten
        glVertex3f( 10.0f,-10.0f, 20.0f);            // unten vorne
        glVertex3f( 10.0f, 10.0f, 20.0f);            // oben vorne
    glEnd();                            // fertig mit Zeichnen der Quads
}

Und bevor ich es vergesse, hier ist die VMatMult Funktion welche einen Vektor mit einer Matrix multipliziert (holen Sie das Mathebuch wieder raus!):

void VMatMult(GLmatrix16f M, GLvector4f v)
{
    GLfloat res[4];                            // enthält die berechneten Ergebnisse
    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];                            // Ergebnisse werden in v[] gespeichert
    v[1]=res[1];
    v[2]=res[2];
    v[3]=res[3];                            // Homogene Koordinate
}

Die Funktion zum Objekt-Laden ist einfach, rufen Sie einfach readObject auf und setzen Sie dann die Verbindungen und die Ebenen-Gleichung für jede Seite.

int InitGLObjects()                            // Initialisiere Objekte
{
    if (!readObject("Data/Object2.txt", obj))            // Lese Object2 in obj
    {
        return FALSE;                        // wenn fehlgeschlagen, gebe False zurück
    }

    setConnectivity(obj);                        // Setze Seite zu Seite Verbindungen

    for ( int i=0;i <obj.nFaces;i++)                // iteriere durch alle Objekt-Seiten
        calculatePlane(obj, obj.pFaces[i]);            // berechne die Ebenen-Gleichung für alle Seiten

    return TRUE;                            // gebe True zurück
}

Zu guter Letzt, KillGLObjects ist eine Funktion für die Bequemlichkeit, so dass wenn Sie mehrere Objekte hinzufügen, können Sie sie an einem zentralen Ort hinzufügen.

void KillGLObjects()
{
    killObject( obj );
}

Alle anderen Funktionen benötigen keine weitere Erklärungen. Ich habe den Standard NeHe Tutorial Code ausgelassen, sowie all die Variablen-Definitionen und die Tastatur-Behandlungs-Funktion. Die Kommentare alleine erklären diese genügend.

Einige Dinge die noch zu diesem Tutorial anzumerken sind:
  • Die Sphere hört nicht auf Schatten auf die Wand zu projezieren. In Wirklichkeit sollte die Sphere auch einen Schatten werfen, was aber nicht auf der Wand zu sehen wäre, weil es versteckt ist. Es ist nur dafür da, was auf gekrümmten Flächen passiert :)
  • Wenn Sie extrem niedrige Frame Raten bemerken, versuchen Sie in den Fullscreen-Modus zu wechseln oder Ihr Desktop Farbtiefe auf 32bpp zu setzen.
  • Arseny L. schreibt: Wenn Sie Probleme mit einer TNT2 im Fenster-Modus haben, stellen Sie sicher, dass Ihre Desktop Farbtiefe nicht auf 16 Bit gesetzt ist. Im 16 Bit Farbmodus wird der Stencil Buffer emuliert, was schludrige Performance nach sich zieht. Es gibt keine Probleme im 32 Bit Modus (Ich habe eine TNT2 Ultra und es überprüft).
Ich muss gesethen, dass es eine langwierige Sache war, dieses Tutorial zu schreiben. Ich hoffe, ihr wisst es zu schätzen, was Jeff hier an Arbeit reinsteckt! Ich hoffe, Sie haben es genossen und ein großer Dank an Banu, der den original Code geschrieben hat! WENN es etwas gibt, was weiterer Erklärung bedarf, können Sie mich (Brett) gerne unterbrettporter@yahoo.com kontaktieren.

* Randy Ridge fügt hinzu: Um die Schatten auf meiner Karte zu sehen, muss die naha Clipping-Ebene auf 0.001f anstatt von 0.1f im ReSizeGLScene() Code gesetzt werden. Der Code wurde in diesem Tutorial geändert und sollte nun auf allen Karten laufen!

Banu Octavian (Choko) & Brett Porter

Jeff Molofee (NeHe)

* DOWNLOAD Visual C++ Code für diese Lektion.

* DOWNLOAD Borland C++ Builder 6 Code für diese Lektion. ( Conversion by Christian Kindahl )
* DOWNLOAD Code Warrior 5.3 Code für diese Lektion. ( Conversion by Scott Lupton )
* DOWNLOAD Delphi Code für diese Lektion. ( Conversion by Felix Hahn )
* DOWNLOAD Dev C++ Code für diese Lektion. ( Conversion by Dan )
* DOWNLOAD Euphoria Code für diese Lektion. ( Conversion by Evan Marshall )
* DOWNLOAD JoGL Code für diese Lektion. ( Conversion by Abdul Bezrati )
* DOWNLOAD KDE/QT Code For This Lesson. ( Conversion by Zsolt Hajdu )
* DOWNLOAD Linux Code für diese Lektion. ( Conversion by Jay Groven )
* DOWNLOAD LWJGL Code für diese Lektion. ( Conversion by Mark Bernard )
* DOWNLOAD Mac OS X/Cocoa Code für diese Lektion. ( Conversion by Bryan Blackburn )
* DOWNLOAD Visual Studio .NET Code für diese Lektion. ( Conversion by Grant James )



Deutsche Übersetzung: Joachim Rohde
Der original Text ist hier zu finden.
Die original OpenGL Tutorials stammen von NeHe's Seite.