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.