NeHe - Lektion 37 - Cel-Shading

Lektion 37



Cel-Shading von Sami "MENTAL" Hamlaoui

Da mich immer noch Leute per E-Mail nach dem Source Code zu dem Artikel fragen, den ich für GameDev.net vor einiger Zeit geschrieben habe und ich absehe, dass eine 2te Version des Artikels (mit Source für jegliches API) noch nicht einmal halbwegs fertig ist, habe ich dieses Tutorial für NeHe zusammengehackt (das war eigentlich die ursprüngliche Absicht des Artikels), so dass allen OpenGL Gurus damit herumspielen können. Entschuldigen Sie die Wahl des Models, aber ich spiele Quake 2 zur Zeit recht ausgiebig... :)

Anmerkung: Der original Artikel zu diesem Code kann unter http://www.gamedev.net/reference/programming/features/celshading gefunden werden.

Dieses Tutorial erklärt eigentlich nicht die Theorie sondern den Code. WARUM es funktioniert, finden Sie im oberen Link. Und nun noch mal laut: HÖRT AUF MIR WEGEN DES SOURCE CODES ZU MAILEN!!!!

Genießt es :).

Als erstes müssen wir ein paar extra Header-Dateien einbinden. Die erste (math.h) ist für die Benutzung der sqrtf (Wurzel) Funktion und die zweite (stdio.h) ist für den Dateizugriff.

#include <math.h>                        // Header Datei für die Math Library
#include <stdio.h>                        // Header Datei für die Standard I/O Library

Nun definieren einige Strukturen, die uns helfen, unsere Daten zu speichern (das spart uns hunderte an Float-Arrays). Die erste ist die tagMATRIX Struktur. Wenn Sie sie genauer betrachten, werden Sie sehen, dass wir die Matrix als ein 1D Array von 16 Floats speichern, anstatt eines 2D 4x4 Arrays. So speichert OpenGL auch seine Matrizen. Wenn wir 4x4 benutzen würden, würden die Werte in der falschen Reihenfolge sein.

typedef struct tagMATRIX                    // Eine Struktur die eine OpenGL Matrix aufnimmt
{
    float Data[16];                        // Wir benutzen [16] wegen des OpenGL's Matrix Format
}
MATRIX;

Als zweites kommt die Vektor-Klasse. Diese speichert einfach die Werte für X, Y und Z.

typedef struct tagVECTOR                    // Eine Struktur die einen einzelnen Vektor enthält
{
    float X, Y, Z;                        // Die Komponenten des Vektors
}
VECTOR;

Als drittes haben wir die Vertex-Struktur. Jeder Vertex benötigt lediglich seinen Normalenvektor und seine Position (keine Textur Ko-Koordinaten). Die MÜSSEN in dieser Reihenfolge gespeichert werden, ansonsten läuft das Laden der Datei schrecklich schief (Ich hab's auf die harte Tour gelernt :(. Das hat mich gelehrt, meinen Code Stückchenweise zu programmieren).

typedef struct tagVERTEX                    // Eine Struktur die einen einzelnen Vertex enthält
{
    VECTOR Nor;                        // Vertex Normalenvektor
    VECTOR Pos;                        // Vertex Position
}
VERTEX;

Zu guter Letzt die Polygon Struktur. Ich weiß, dass das ein ziemlich dummer Weg ist, Vertexes zu speicher, aber trotz der Einfachkeit funktioniert es perfekt. Normalerweise würde ich ein Array von Vertices verwenden, ein Array von Polygonen und die Indexnummer der 3 Vertices in der Polygon-Struktur speichern, aber auf diese Art ist es einfacher Ihnen zu zeigen, wie das ganze läuft.

typedef struct tagPOLYGON                    // Eine Struktur die ein einzelnes Polygon enthält
{
    VERTEX Verts[3];                    // Array von 3 VERTEX Strukturen
}
POLYGON;

Es folgt weiterhin ziemlich einfacher Code. Schauen Sie sich die Kommentare für eine Erklärung der jeweiligen Variable an.

bool        outlineDraw    = true;                // Flag um den Umriß zu zeichnen
bool        outlineSmooth    = false;            // Flag Anti-Alias für die Linien anzuwenden
float        outlineColor[3]    = { 0.0f, 0.0f, 0.0f };        // Farbe der Linien
float        outlineWidth    = 3.0f;                // Breite der Linien

VECTOR        lightAngle;                    // Die Richtung des Lichts
bool        lightRotate    = false;            // Flag um zu sehen, ob wir das Licht rotieren

float        modelAngle    = 0.0f;                // Y-Achse Winkel des Modells
bool        modelRotate    = false;            // Flag um das Modell zu rotieren

POLYGON        *polyData    = NULL;                // Polygon Daten
int        polyNum        = 0;                // Anzahl der Polygone

GLuint        shaderTexture[1];                // Speicherplatz für eine Textur

Das ist das einfachste Model-Datei-Format. Die ersten paar Bytes speichern die Anzahl der Polygone in der Szene und der Rest der Datei ist ein Array von tagPOLYGON Strukturen. Darum können die Daten auch ausgelesen werden, ohne sie in eine bestimmte Reihenfolge zu sortieren.

BOOL ReadMesh ()                        // liest den Inhalt der Datei "model.txt" 
{
    FILE *In = fopen ("Data\\model.txt", "rb");        // öffne die Datei

    if (!In)
        return FALSE;                    // gebe FALSE zurück, wenn Datei nicht geöffnet wurde

    fread (&polyNum, sizeof (int), 1, In);            // lese den Header (z.B. Anzahl der Polygone)

    polyData = new POLYGON [polyNum];            // Alloziiere den Speicher

    fread (&polyData[0], sizeof (POLYGON) * polyNum, 1, In);// lese alle Polygon-Daten ein

    fclose (In);                        // Schließe die Datei

    return TRUE;                        // hat funktioniert
}

Einige grundlegende mathematische Funktionen nun. Das Skalarprodukt (DotProdukt) berechnet den Winkel zwischen zwei Vektoren oder Ebenen, die Magnitude Funktion berechnet die Länge des Vektors und die Normalize Funktion reduziert den Vektor auf eine Längeneinheit von 1.

inline float DotProduct (VECTOR &V1, VECTOR &V2)        // berechne den Winkel zwischen 2 Vektoren
{
    return V1.X * V2.X + V1.Y * V2.Y + V1.Z * V2.Z;        // gebe den Winkel zurück
}

inline float Magnitude (VECTOR &V)                // berechne die Länge des Vektors
{
    return sqrtf (V.X * V.X + V.Y * V.Y + V.Z * V.Z);    // gebe die Länge des Vektors zurück
}

void Normalize (VECTOR &V)                    // erzeugt einen Vektor mit einer Längeneinheit von 1
{
    float M = Magnitude (V);                // berechne die Länge des Vektors

    if (M != 0.0f)                        // Stelle sicher, dass wir nicht durch 0 dividieren
    {
        V.X /= M;                    // Normalisiere die 3 Komponenten
        V.Y /= M;
        V.Z /= M;
    }
}

Diese Funktion rotiert einen Vektor um die angegebene Matrix. Beachten Sie bitte, dass sie den Vektor NUR rotiert - das hat nichts mit der Position des Vektors zu tun. Das wird verwendet, wenn Normalenvektoren rotiert werden, um sicher zu gehen, dass sie weiterhin in die richtige Richtung zeigen, wenn wir die Beleuchtung berechnen.

void RotateVector (MATRIX &M, VECTOR &V, VECTOR &D)        // Rotiere einen Vektor um die angegebene Matrix
{
    D.X = (M.Data[0] * V.X) + (M.Data[4] * V.Y) + (M.Data[8]  * V.Z);    // Rotiere um die X Achse
    D.Y = (M.Data[1] * V.X) + (M.Data[5] * V.Y) + (M.Data[9]  * V.Z);    // Rotiere um die Y Achse
    D.Z = (M.Data[2] * V.X) + (M.Data[6] * V.Y) + (M.Data[10] * V.Z);    // Rotiere um die Z Achse
}

Die erste Hauptfunktion der engine, Initialize, macht genau das, was sie sagt. Sie initialisiert. Ich habe ein paar Codezeilen weggelassen, da sie keiner Erklärung bedürfen.

// Jeder GL Init Code & Benutzer Initialiasierung kommt hier hin
BOOL Initialize (GL_Window* window, Keys* keys)
{

Diese 3 Variablen werden zum laden der Shader Datei verwendet. Line enthält Platz für eine einzelne Zeile aus der Textdatei, während ShaderDate die aktuellen Shader-Werte speichert. Sie werden sich vielleicht wundern, warum wir 96 Werte statt 32 haben. Nun, wir müssen die grauskalierten Werte in RGB konvertieren, so dass OpenGL sie verwenden kann. Wir können die Werte immer noch als Grauskalierung speichern, aber wir werden einfach den selben Wert für die R, G und B Komponente verwenden, wenn wir sie auf die Textur bringen.

    char Line[255];                        // Speicherplatz für 255 Zeichen
    float shaderData[32][3];                // Speicherplatz für die 96 Shader Werte

    FILE *In = NULL;                    // Datei Zeiger

Wenn wir die Zeilen zeichnen, wollen wir sicher gehen, dass diese nett und weich aussehen. Am Anfang ist dieser Wert deaktiviert, aber indem Sie die Taste "2" drücken, können Sie den Wert hin und her wechseln.

    glShadeModel (GL_SMOOTH);                // aktiviert weiches Farb-Shading
    glDisable (GL_LINE_SMOOTH);                // deaktiviere anfangs das Weichzeichnen von Linien

    glEnable (GL_CULL_FACE);                // aktiviere OpenGL Face Culling

Wir deaktivieren OpenGLs Beleuchtung, da wir die Beleuchtungsberechnungen selbst vornehmen.

    glDisable (GL_LIGHTING);                // deaktiviere OpenGL Beleuchtung

Hier laden wir die Shader-Datei. Das sind einfach 32 Fließkommawerte als ASCII gespeichert (zur einfachen Veränderung), jeder in einer eigenen Zeile.

    In = fopen ("Data\\shader.txt", "r");            // öffne die Shader-Datei

    if (In)                            // überprüfe, ob die Datei geöffnet wurde
    {
        for (i = 0; i <32; i++)            // durchlaufe alle 32 grauskalierte Werte
        {
            if (feof (In))                // Überprüfe auf das Ende der Datei
                break;

            fgets (Line, 255, In);            // hole die aktuelle Zeile

Hier konvertieren wir den Grauskalierungswert in RGB, so wie es oben beschrieben wurde.

            // kopiere über den Wert
            shaderData[i][0] = shaderData[i][1] = shaderData[i][2] = atof (Line);
        }

        fclose (In);                    // Schließe die Datei
    }

    else
        return FALSE;                    // Das lief furchtbar schief

Nun laden wir die Textur. Wie klar zu sehen ist, sollten sie keine Filter für die Textur benutzen, da sie ansonsten recht seltsam aussehen wird, um es mal milde auszudrücken. GL_TEXTURE_1D wird verwendet, da wir ein 1D Array von Werten verwenden.

    glGenTextures (1, &shaderTexture[0]);            // hole eine freie Textur ID

    glBindTexture (GL_TEXTURE_1D, shaderTexture[0]);    // Binde diese Textur. Von nun an wird sie 1D sein

    // Um es nochmals zu betonen: Lassen Sie OpenGL keine Bi/Trilineare Filter verwenden!
    glTexParameteri (GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri (GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

    // Upload
    glTexImage1D (GL_TEXTURE_1D, 0, GL_RGB, 32, 0, GL_RGB , GL_FLOAT, shaderData);

Nun setzen Sie die Beleuchtungs-Richtung. Ich lasse sie die positive Z-Achse runter zeigen, was bedeutet, dass das Model direkt von vorne 'getroffen' wird.

    lightAngle.X = 0.0f;                    // Setze die X Richtung 
    lightAngle.Y = 0.0f;                    // Setze die Y Richtung
    lightAngle.Z = 1.0f;                    // Setze die T Richtung

    Normalize (lightAngle);                    // Normalisiere die Licht Richtung

Lade Mesh aus der Datei (wie oben beschrieben).

    return ReadMesh ();                    // gebe den Wert von ReadMesh zurück
}

Das Gegenteil der obigen Funktion, Deinitialize löscht die Textur und Polygon Daten, die von Initialize und ReadMesh erzeugt wurden.

void Deinitialize (void)                    // Jede Benutzer DeInitialisation kommt hier rein
{
    glDeleteTextures (1, &shaderTexture[0]);        // lösche die Shader-Textur

    delete [] polyData;                    // lösche die Polygon-Daten
}

Die Hauptschleife der Demo. Alles was sie macht, ist die Abarbeitung der Eingaben und die Aktuallisierung des Winkels. Steuerung ist wie folgt:

<Leertaste> = Rotation an/aus

1 = Umriß zeichnen an/aus

2 = Umriß Anti-Aliasing an/aus

<HOCH> = verbreitere Linienbreite

<RUNTER> = verringere Linienbreite



void Update (DWORD milliseconds)                // die Aktualisierung der Bewegung erfolgt hier
{
    if (g_keys->keyDown [' '] == TRUE)            // Wurde die Leertaste gedrückt?
    {
        modelRotate = !modelRotate;            // Schalte Rotation an/aus

        g_keys->keyDown [' '] = FALSE;
    }

    if (g_keys->keyDown ['1'] == TRUE)            // Ist die Taste 1 gedrückt worden?
    {
        outlineDraw = !outlineDraw;            // Schalte das Zeichnen des Umrisses an/aus

        g_keys->keyDown ['1'] = FALSE;
    }

    if (g_keys->keyDown ['2'] == TRUE)            // Ist die Taste 2 gedrückt worden?
    {
        outlineSmooth = !outlineSmooth;            // Schalte Anti-Aliasing an/aus

        g_keys->keyDown ['2'] = FALSE;
    }

    if (g_keys->keyDown [VK_UP] == TRUE)            // Wurde die Pfeil-nach-oben Taste gedrückt?
    {
        outlineWidth++;                    // Erhöhe Linienbreite

        g_keys->keyDown [VK_UP] = FALSE;
    }

    if (g_keys->keyDown [VK_DOWN] == TRUE)            // Wurde die Pfeil-nach-unten Taste gedrückt?
    {
        outlineWidth--;                    // vermindere die Linienbreite

        g_keys->keyDown [VK_DOWN] = FALSE;
    }

    if (modelRotate)                    // überprüfe ob Rotation aktiviert ist
        modelAngle += (float) (milliseconds) / 10.0f;    // aktualisiere Winkel basierend auf der Zeit
}

Die Funktion auf die Sie alle gewartet haben. Die Draw Funktion macht alles - berechnet die Shade-Werte, rendert die Mesh, rendert den Umriß und, naja, das ist es.

void Draw (void)
{

TmpShade wird benutzt, um die Shader Werte für den aktuellen Vertex zu speichern. Alle Vertex-Daten werden zur selben Zeit berechnet, was bedeutet, dass wir nur eine einzelne Variable benutzen müssen, die wir immer wieder verwenden können.

Die TmpMatrix, TmpVector und TmpNormal Strukturen werden ebenso verwendet, um die Vertex-Daten zu berechnen. TmpMatrix wird einmal am Anfang der Funktion gesetzt und wird solange nicht verändert, bis Draw erneut aufgerufen wird. TmpVector und TmpNormal auf der anderen Seite, ändern sich, wenn ein weiterer Vertex bearbeitet wird.

    float TmpShade;                        // Temporärer Shader Wert

    MATRIX TmpMatrix;                    // Temporäre MATRIX Struktur
    VECTOR TmpVector, TmpNormal;                // Temporäre VECTOR Strukturen

Löschen wir den Buffer und die Matrix-Daten.

    glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);    // Lösche die Buffer
    glLoadIdentity ();                    // Resette die Matrix

Die erste Überprüfung dient dazu, ob wir weiche Umrisse haben wollen. Wenn ja, schalten wir Anti-Aliasing an. Wenn, schalten wir es aus. Ganz einfach!

    if (outlineSmooth)                    // überprüfe, ob wir Anti-Aliased Linien haben wollen
    {
        glHint (GL_LINE_SMOOTH_HINT, GL_NICEST);    // benutze die guten Berechnungen
        glEnable (GL_LINE_SMOOTH);            // aktiviere Anti-Aliasing
    }

    else                            // Wir wollen keine weichen Linien
        glDisable (GL_LINE_SMOOTH);            // deaktiviere Anti-Aliasing

Dann initialisieren wir den Viewport. Wir bewegen die Kamera um 2 Einheiten nach hinten und rotieren dann das Modell um den Winkel. Anmerkung: da wir erst die Kamera bewegen, wird das Modell im Spot rotieren. Wenn wir es andersherum machen, würde das Modell um die Kamera herum kreisen.

Wir holen uns dann die von OpenGL neu erzeugte Matrix und speichen sie in TmpMatrix.

    glTranslatef (0.0f, 0.0f, -2.0f);            // bewege 2 Einheiten in den Bildschirm hinein
    glRotatef (modelAngle, 0.0f, 1.0f, 0.0f);        // Rotatiere das Modell auf seiner Y-Achse

    glGetFloatv (GL_MODELVIEW_MATRIX, TmpMatrix.Data);    // hole die erzeugte Matrix

Der Zauber beginnt. Als erstes aktivieren wir 1D texturierung und dann aktivieren wir die Shader Textur. Das wird von OpenGL als Look-Up-Tabelle verwendet. Dann setzen wir die Farbe des Modells (weiß). Ich habe weiß gewählt, da es die Hervorhebungen und das Shading sehr viel besser zeigen, als andere Farben. Ich rate Ihnen, nicht schwarz zu verwenden :)

    // Cel-Shading Code
    glEnable (GL_TEXTURE_1D);                // aktiviere 1D Texturing
    glBindTexture (GL_TEXTURE_1D, shaderTexture[0]);    // Binde unsere Textur

    glColor3f (1.0f, 1.0f, 1.0f);                // Setze die Farbe des Modells

Nun fangen wir an, die Dreiecke zu zeichnen. Wir gehen durch jedes Polygon in dem Array und dann darauf durch die entsprechenden Vertices. Der erste Schritt ist das Kopieren der Normalenvektor Informationen in die temporäre Struktur. Dadurch können wir die Normalenvektoren rotieren, aber so bleiben die original Werte erhalten (keine Verminderung der Genauigkeit).

    glBegin (GL_TRIANGLES);                    // Teile OpenGL mit, dass wir Dreiecke zeichnen werden

        for (i = 0; i <polyNum; i++)            // durchlaufe jedes Polygon
        {
            for (j = 0; j <3; j++)            // durchlaufe jeden Vertex
            {
                TmpNormal.X = polyData[i].Verts[j].Nor.X;    // Fülle die TmpNormal Struktur mit den
                TmpNormal.Y = polyData[i].Verts[j].Nor.Y;    // Normalenvektorwerten der Vertices
                TmpNormal.Z = polyData[i].Verts[j].Nor.Z;

Als zweites rotieren wir den Normalenvektor um die vorher von OpenGL geholte Matrix. Wir normalisieren dann, so dass nichts schief läuft.

                // Rotiere das um die Matrix
                RotateVector (TmpMatrix, TmpNormal, TmpVector);

                Normalize (TmpVector);        // Normalisiere den neuen Normalenvektor

Als drittes ermitteln wir das Skalarprodukt der rotierten Normalenvektoren und Licht-Richtung (heißt lightAngle, da ich vergessen habe, dass aus meiner alten Beleuchtungs-Klasse zu ändern). Wir passen den Wert dann an, so dass dieser in einem Wertebereich zwischen 0 und 1 liegt (von -1 bis +1).

                // berechne den Shade Wert
                TmpShade = DotProduct (TmpVector, lightAngle);

                if (TmpShade <0.0f)
                    TmpShade = 0.0f;    // setze den Wert auf 0, wenn negativ

Als viertes übergeben wir diesen Wert an OpenGL als Textur Ko-Koordinate. Die Shader-Textur spielt dabei die Look-up-Tabelle (der Shader-Wert ist der Index), was der Hauptgrund (meines Erachtens nach) ist, warum 1D Texturen erfunden wurden. Wir übergeben die Vertex Positionen dann an und Opengl und wiederholen. Und wiederholen. Und wiederholen. Ich glaube, Sie wissen was gemeint ist.

                glTexCoord1f (TmpShade);    // Setze die Textur Ko-Koordinate wie den Shade Wert
                // Sende die Vertex Position
                glVertex3fv (&polyData[i].Verts[j].Pos.X);
            }
        }

    glEnd ();                        // Telie OpenGL mit, dass wir fertig mit Zeichnen sind

    glDisable (GL_TEXTURE_1D);                // deaktiviere 1D Textur

Nun machen wir mit dem Umriß weiter. Ein Umriß kann als "eine Ecke wo ein Polygon nach vorne zeigt und ein anderes abgewandt ist" definiert werden. In OpenGL ist es dort, wo der Depth Test auf weniger oder gleich (GL_LEQUAL) dem aktuellen Wert gesetzt wird und wenn alle nach vorne zeigenden Seiten ausgewählt werden. Wir blenden auch die Linien ein, damit es gut aussieht :)

So, wir aktivieren Blending und setzen den Blend-Modus. Wir teilen OpenGL mit, dass abgewandte Polygone als Linien gerendert werden sollen und setzen die Breite dieser Linien. Wir wählen alle nach vorne gerichteten Polygone aus und setzen den Depth Test auf weniger oder gleich unserem aktuellen Z-Wert. Danach wird die Farbe der Linie gesetzt und wir durchlaufen alle Polygone und zeichnen seine Vertices. Wir müssen lediglich die Vertex Position übergeben und die nicht den Normalenvektor oder der Shade-Wert, da wir lediglich einen Umriß haben wollen.

    // Outline Code
    if (outlineDraw)                    // Überprüfe ob wir den Umriß zeichnen wollen
    {
        glEnable (GL_BLEND);                // aktiviere Blending
        // Setze den Blend Modus        
        glBlendFunc (GL_SRC_ALPHA ,GL_ONE_MINUS_SRC_ALPHA);

        glPolygonMode (GL_BACK, GL_LINE);        // zeichne abgewandte Polygone als Drahtgitter
        glLineWidth (outlineWidth);            // Setze die Linienbreite

        glCullFace (GL_FRONT);                // zeichne keine nach vorn gerichtetenPolygone

        glDepthFunc (GL_LEQUAL);            // ändere den Depth Modus

        glColor3fv (&outlineColor[0]);            // Setze die Umriß-Farbe

        glBegin (GL_TRIANGLES);                // Teile OpenGL mit, was wir zeichnen wollen

            for (i = 0; i <polyNum; i++)        // durchlaufe jedes Polygon
            {
                for (j = 0; j <3; j++)        // durchlaufe jeden Vertex
                {
                    // Sende die Vertex Position
                    glVertex3fv (&polyData[i].Verts[j].Pos.X);
                }
            }

        glEnd ();                    // Teile OpenGL mit, dass wir fertig sind

Danach setzen wir alle zurück, so wie es war und beenden.

        glDepthFunc (GL_LESS);                // Resette den Depth-Test Modus

        glCullFace (GL_BACK);                // Resette die Seite, die ausgewählt werden soll

        glPolygonMode (GL_BACK, GL_FILL);        // Resette den abgewandte Polygone Zeichnen-Modus

        glDisable (GL_BLEND);                // deaktiviere Blending
    }
}

Wie Sie sehen, ist Cel-Shading nicht so schwer. Natürlich können diese Techniken noch um ein vielfaches verbessert werden. Ein gutes Beispiel ist das Spiel XIII http://www.nvidia.com/object/game_xiii.html, welches Sie in eine Cartoon-Welt versetzt. Wenn Sie tiefer in die Materie der Cartoon Rendering Techniken einsteigen wollen, sollten Sie einen Blick in das Buch Real-time Rendering (Möller, Haines) werfen und zwar in das Kapitel "Non-Photorealistic Rendering". Wenn Sie es bevorzugen, Artikel im Internet zu lesen, können Sie eine große Linkliste hier finden: http://www.red3d.com/cwr/npr/

Sami Hamlaoui (MENTAL)

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 Michal Tucek )
* DOWNLOAD Dev C++ Code für diese Lektion. ( Conversion by Warren Moore )
* DOWNLOAD Euphoria Code für diese Lektion. ( Conversion by Evan Marshall )
* DOWNLOAD JoGL Code für diese Lektion. ( Conversion by Abdul Bezrati )
* DOWNLOAD Linux / GLut Code für diese Lektion. ( Conversion by Kah )
* DOWNLOAD Linux/GLX Code für diese Lektion. ( Conversion by Patrick Schubert )
* DOWNLOAD Linux/SDL Code für diese Lektion. ( Conversion by Sean Farrell )
* 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.