Lektion 41
Willkommen zu einem weiteren Spaß-geladenem Tutorial. Dieses Mal werde ich versuchen Volumetrischen Nebel zu erklären, den wir mit der glFogCoordf Extension erzeugen. Um dieses Demo laufen lassen zu können, muss Ihre Grafikkarte die "GL_EXT_fog_coord" Extension unterstützen. Wenn Sie sich nicht sicher sind, ob Ihre Karte diese Extension unterstützt, haben Sie zwei Möglichkeiten... 1) laden Sie den VC++ Source Code runter und schauen Sie, ob er läuft. 2) laden Sie Lektion 24 runter und scrollen Sie durch die Liste der Extensionen, die von Ihrer Grafikkarte unterstützt werden.
Dieses Tutorial wird Sie in den NeHe IPicture Code einführen, welcher in der Lage ist, BMP, EMF, GIF, ICO, JPG und WMF Dateien von Ihrem Computer oder einer Web-Seite zu laden. Sie werden ebenfalls lernen, wie man die "GL_EXT_fog_coord" Extension verwendet, um einen wirklich coolen volumetrischen Nebel zu erzeugen (Nebel, der in einem begrenzten Raum schweben kann ohne den Rest der Szene zu beeinträchtigen).
Wenn dieses Tutorial nicht auf Ihrere Maschine laufen sollte, sollten Sie als erstes überprüfen, dass Sie die neuesten Grafikkarten-Treibe installiert haben. Wenn Sie die neusten Treiber haben und das Demo immer noch nicht läuft... sollten Sie sich vielleicht eine neue Grafikkarte zulegen. Eine Low-End GeForce 2 lang vollkommen aus und sollte nicht allzuviel kosten. Wenn Ihre Karte die Fog-Extension nicht unterstützt, wer weiß, welche anderen Extensionen sie nicht unterstützt?
Für die unter Ihnen, die das Demo nicht laufen lassen können und sich ausgeschlossen fühlen... behalten Sie folgedens im Hinterkopf: Jeden einzelnen Tag bekommen ich mindestens eine E-Mail mit einer Anfrage für ein neues Tutorial. Viele der geforderten Tutorials sind bereits online! Die Leute kümmern sich nicht darum, was bereits online ist und überlesen in der Eile die Tutorials, an denen sie interessiert sind. Andere Tutorials sind zu komplex und würden mich wochenlanges Programmieren kosten. Und dann gibt es noch die Tutorials, die ich schreiben könnte, die ich aber in der Regel vermeide, weil sie nicht auf allen Karten laufen. Nun sind diese Karten, wie die GeForce aber so billig, dass jeder mit etwas Taschengeld sich diese leisten kann, ich kann es nicht länger rechtfertigen diese Tutorials nicht zu schreiben. Um ehrlich zu sein, wenn Ihre Grafikkarte nur die Basis-Extensionen unterstützt, ist diese hoffnunglos veraltet! Und wenn ich weiterhin Themen wie Extensionen auslassen, werden die Tutorials hinterherhinken!
Nachdem das gesagt wurde... Fangen wir mit dem Code an!!!
Der Code fängt sehr ähnlich wie der alte Basecode an und ist fast identisch mit dem neuen NeHeGL Basecode. Der einzige Unterschied ist die extra Zeile Code, um die OLECTL Header Datei zu inkludieren. Dieser Header muss inkludiert werden, wenn Sie den IPICTURE Code benutzen wollen. Wenn Sie diese Zeile exkludieren, werden Sie Fehler erhalten, wenn Sie versuchen IPicture, OleLoadPicturePath und IID_IPicture zu verwenden.
Wie im NeHeGL Basecode benutzen wir #pragma comment ( lib, ... ) um die benötigten Library Dateien automatisch zu inkludieren! Beachten Sie, dass wir nicht länger die glaux Library inkludieren müssen (Ich bin sicher, dass einige von Ihnen sich jetzt freuen).
Die nächsten drei Zeilen Code überprüfen, ob CDS_FULLSCREEN definiert ist. Wenn nicht (was bei den meisten Compilern der Fall ist), setzen wir diesen Wert auf 4. Ich weiß, dass viele von Ihnen mir gemailt haben und mich gefragt haben, warum Sie Fehler erhalten, wenn Sie versucht haben, Code mit Dev C++ zu kompilieren, der CDS_FULLSCREEN enthielt. Fügen Sie diese drei Zeilen ein und Sie werden diesen Fehler nicht mehr erhalten!
#include <windows.h> // Header Datei für Windows
#include <gl\gl.h> // Header Datei für die OpenGL32 Library
#include <gl\glu.h> // Header Datei für die GLu32 Library
#include <olectl.h> // Header Datei für die OLE Controls Library (wird in BuildTexture verwendet)
#include <math.h> // Header Datei für die Math Library (wird in BuildTexture verwendet)
#include "NeHeGL.h" // Header Datei für NeHeGL
#pragma comment( lib, "opengl32.lib" ) // Suche nach OpenGL32.lib während des Linkens
#pragma comment( lib, "glu32.lib" ) // Suche nach GLu32.lib während des Linkens
#ifndef CDS_FULLSCREEN // CDS_FULLSCREEN ist von eineigen Compilerns nicht definiert
#define CDS_FULLSCREEN 4 // Indem wir es hier definieren,
#endif // können wir Fehler vermeiden
GL_Window* g_window; // Fenster Struktur
Keys* g_keys; // Keyboard
Im folgenden Code setzen wir die Farbe unseres Nebels. In diesem Fall wollen wir ein dunkel-Orange. Ein wenig rot (0.6f) gemicht mit etwas weniger Grün (0.3f) wird uns die gewünschte Farbe liefern.
Die Fließkomma Variable camz wird später im Code verwendet, um unsere Kamera innerhalb des langen und dunklen Korridors zu verwenden! Wir werden uns vor und zurück durch diesen Korridor bewegen, indem wir auf der Z-Achse translatieren, bevor wir den Korridor zeichnen.
// Benutzerdefinierte Variablen
GLfloat fogColor[4] = {0.6f, 0.3f, 0.0f, 1.0f}; // Nebel Farbe
GLfloat camz; // Kamera Z Tiefe
So wie CDS_FULLSCREEN einen vordefinierten Wert von 4 hat... so haben die Variablen GL_FOG_COORDINATE_SOURCE_EXT und GL_FOG_COORDINATE_EXT ebenfalls vordefinierte Werte. Wie in den Kommentaren erwähnt, wurden die Werte aus der GLEXT Header-Datei genommen. Eine Datei, die frei im Internet verfügbar ist. Vielen Dank an Lev Povalahev, der eine solch wertvolle Header-Datei erstellt hat! Diese Werte müssen gesetzt sein, wenn Sie den Code kompilieren wollen! Das Endresultat ist, dass wir zwei neue verfügbare Aufzählungen haben (GL_FOG_COORDINATE_SOURCE_EXT & GL_FOG_COORDINATE_EXT).
Um die Funktion glFogCoordfExt zu verwenden, müssen wir einen Funktions-Prototyp typedef deklarieren, welcher dem Extension's Einstiegspunkt entspricht. Klingt kompliziert, ist aber nicht so schlimm. Auf deutsch... wir müssen unserem Programm die Anzahl der Parameter und die Art der jeweiligen Parameter mitteilen, die von der Funktion glFogCoordfEXT akzeptiert werden. In diesem Fall... übergeben wir einen Parameter der Funktion und dieser ist ein Fließkommawert (eine Koordinate).
Als nächstes müssen wir eine globale Variable vom selben Typen wie dem des Funktions-Prototyps typedef deklarieren. In diesem Fall PFNGLFOGCOORDFEXTPROC. Das ist der erste Schritt, um unsere neue Funktion zu erzeugen (glFogCoordfEXT). Sie ist global, so dass wir den Befehl überall in unserem Code verwenden können. Der Name sollte exakt dem tatsächlichen Extensions-Namen entsprechen. Der tatsächliche Extensions-Name ist glFogCoordfEXT und der Name den wir verwenden ist ebenfalls glFogCoordfEXT.
Wenn wir erst einmal wglGetProcAddress verwendet haben, um die Funktions-Variable der Adresse der OpenGL Treiber Extensions-Funktion zuzuordnen, können wir glFogCoordfEXT wie eine normale Funktion aufrufen. Mehr dazu später!
Die letzte Zeile bereitet die Dinge für unsere einzige Textur vor.
Was wir bisher haben...
Wir wissen, dass PFNGLFOGCOORDFEXTPROC ein Fließkommawert übergeben wird (GLfloat coord).
Da glFogCoordfEXT vom Typen PFNGLFOGCOORDFEXTPROC ist, ist es sicher zu sagen, dass glFogCoordfEXT nur einen Fließkommawert erwartet... woraus sich glFogCoordfEXT(GLfloat coord) ergibt.
Unser Funktion ist definiert, aber wird nichts machen, da glFogCoordfEXT zur Zeit NULL ist (wir müssen immer noch glFogCoordfEXT mit der Adresse der OpenGL Treiber Extensions Funktion verbinden).
Ich hoffe wirklich, dass das verständlich ist... es ist sehr einfach, wenn man weiß, wie es funktioniert... aber es zu beschreiben ist extrem schwierig (zumindest für mich). Wenn irgendwer diesen Text-Abschnitt erneut schreiben möchte, mit simpler / nicht komplizierter Wortwahl, schicken Sie mir bitte eine E-Mail! Der einzige Weg, wie ich es besser erklären könnte, wäre mit Bildern und zur Zeit möchte ich dieses Tutorial schleunigst online bringen!
// Variables Necessary For FogCoordfEXT
#define GL_FOG_COORDINATE_SOURCE_EXT 0x8450 // Wert aus GLEXT.H
#define GL_FOG_COORDINATE_EXT 0x8451 // Wert aus GLEXT.H
typedef void (APIENTRY * PFNGLFOGCOORDFEXTPROC) (GLfloat coord); // Deklariere Funktions Prototyp
PFNGLFOGCOORDFEXTPROC glFogCoordfEXT = NULL; // unsere glFogCoordfEXT Function
GLuint texture[1]; // eine Textur (für die Wände)
Nun zum spaßigen Teil... dem eigentlichen Code, der ein Bild in eine Textur verwandelt mit der Hilfe der Magie von IPicture :)
Diese Funktion benötigt einen Pfadnamen (Pfad zum eigentlichen Bild, das wir laden wollen... entweder ein Dateiname oder eine Web URL) und eine Textur ID (zum Beispiel ... texture[0]).
Wir müssen einen Device Context für unser temporäres Bitmap erstellen. Wir benötigen ebenso einen Platz um die Bitmapdaten (hbmpTemp) zu speichern, eine Verbindung zum IPicture Interface, Variablen um den Pfad (Datei oder URL) zu speichern. 2 Variablen, um die Bild-Breite und 2 Variable um die Bild-Höhe zu speichern. lwidth und lheight speichern die aktuelle Breite und Höhe. lwidthpixels und lheightpixels speichern die Höhe und Breite in Pixeln, um diese gegebenenfalls der maximalen Texturgröße der Grafikkarte anzupassen. Die maximale Textur-Größe wird in glMaxTexDim gespeichert.
int BuildTexture(char *szPathName, GLuint &texid) // Lade Bild und konvertiere in Texturen
{
HDC hdcTemp; // Der DC der unser Bitmap enthält
HBITMAP hbmpTemp; // enthält temporär das Bitmap
IPicture *pPicture; // IPicture Interface
OLECHAR wszPath[MAX_PATH+1]; // gesamter Pfad zum Bild (WCHAR)
char szPath[MAX_PATH+1]; // gesamter Pfad zum Bild
long lWidth; // Breite in logischen Einheiten
long lHeight; // Höhe in logischen Einheiten
long lWidthPixels; // Breite in Pixel
long lHeightPixels; // Höhe in Pixel
GLint glMaxTexDim ; // enthält die maximale Texturgröße
Der nächste Codeabschnitt übprüft, ob der Dateiname eine URL oder ein Dateipfad ist. Wir machen das, indem wir überprüfen, ob er http:// enthält. Wenn derDateiname eine Web URL ist, kopieren wir den Namen nach szPath.
Wenn der Dateiname keine URL enthält, ermitteln wir das aktuelle Verzeichnis. Wenn Sie das Demo unter C:\wow\lesson41 gespeichert haben und Sie versuchen data\wall.bmp zu laden, muss das Programm den komplette Pfad zu wall.bmp wissen, es langt nicht zu wissen, dass die Bmp-Datei in einem Verzeichnis namens data gespeichert ist. GetCurrentDirectory liefert uns den aktuellen Pfad. Der Ort, an dem sowohl die .EXE als auch das 'data' Verzeichnis gespeichert ist.
Wenn die .exe unter "C:\wow\lesson41" gespeichert ist... würde "C:\wow\lesson41" als das Arbeitsverzeichnis zurückgegeben werden. Wir müssen "\\" am Ende des Arbeitsverzeichnisses anhängen, sowie "data\wall.bmp". Die "\\" repräsentieren einen einzelnen "\". Wenn wir also alles zusammenfügen, erhalten wir "c:\wow\lesson41" + "\" + "data\wall.bmp"... oder "c:\wow\lesson41\data\wall.bmp". Verstanden?
if (strstr(szPathName, "http://")) // wenn der Pfad http:// enthält, dann...
{
strcpy(szPath, szPathName); // Hänge den Pfadnamen an szPath an
}
else // Ansonsten... Laden wir eine Datei
{
GetCurrentDirectory(MAX_PATH, szPath); // ermittle unser Arbeitsverzeichnis
strcat(szPath, "\\"); // Hänge "\" an das Arbeistverzeichnis an
strcat(szPath, szPathName); // hänge den Pfadnamen an
}
Nun haben wir den kompletten Pfad in szPath gespeichert. Nun müssen wir den Pfadnamen von ASCII nach Unicode umwandeln, so dass OleLoadPicturePath den Pfadnamen auch versteht. Die erste folgende Codezeile macht das für uns. Das Ergebniss wird in zszPath gespeichert.
CP_ACP bedeutet ANSI Codepage. Der zweite Parameter spezifiziert das Handling von unmapped Zeichen (im folgenden Code ignorieren wir diesen Parameter). szPath ist der Wide-Character String, der kovertiert werden soll. Der 4te Parameter ist die Breite des Wide-Character Strings. Wenn der Wert auf -1 gesetzt wird, wird angenommen das der String null-terminiert ist (was er ist). wszPath ist der Ort, an dem der konvertierte String gespeichert wird und MAX_PATH die maximale Größe unseres Dateipfads (256 Zeichen).
Nachdem der Pfad in Unicode konvertiert wurde, versuchen wir das Bild mit OleLoadPicturePath zu laden. Wenn alles glatt läuft, zeigt pPicture auf die Bilddaten und der Ergebniscode wird in hr gespeichert.
Wenn das Laden fehl schlug, wird das Programm beendet.
MultiByteToWideChar(CP_ACP, 0, szPath, -1, wszPath, MAX_PATH); // konvertiere von ASCII nach Unicode
HRESULT hr = OleLoadPicturePath(wszPath, 0, 0, 0, IID_IPicture, (void**)&pPicture);
if(FAILED(hr)) // wenn das Laden fehl schlug
return FALSE; // gebe False zurück
Nun müssen wir einen temporären Device Context erzeugen. Wenn alles klappt, enthält hdcTemp den kompatiblen Device Context. Wenn das Programm keinen kompatiblen Device Context erzeugen kann, wird pPicture freigegeben und das Programm beendet.
hdcTemp = CreateCompatibleDC(GetDC(0)); // erzeuge den Windows kompatiblen Device Context
if(!hdcTemp) // schlug die Erzeugung fehl?
{
pPicture->Release(); // dekrementiere IPicture Reference Zähler
return FALSE; // gebe False zurück(Fehlschlag)
}
Nun ist es an der Zeit die Grafikkarte abzufragen und herauszufinden, was die maximal unterstützte Textur-Dimension ist. Dieser Code ist wichtig, da er versucht das Bild auf allen Grafikkarten gut aussehen zu lassen. Er wird nicht nur das Bild zu einer Größe zu Basis 2 verändern. Er wird das Bild auch so ändern, dass es in Ihren Grafikkartenspeicher passt. Das erlaubt Ihnen Bilder mit jeglicher Breite oder Höhe zu laden. Der einzige Haken ist, dass Benutzer mit schlechten Grafikkarten sehr viele Details verloren gehen, wenn Sie versuchen, hochauflösende Bilder anzuzeigen.
Auf zum Code... wir benutzen glGetIntegerv(...) um die maximale Textur-Dimension (256, 512, 1024, etc) zu ermitteln, die von der Grafikkarte unterstützt wird. Dann überprüfen wir, welche Breite das Bild hat. pPicture->get_width(&lwidth) ist die Breite des Bildes.
Wir benutzen etwas trickreiche Mathematik, um die Bild-Breite in Pixel zu konvertieren. Das Ergebniss wird in lWidthPixels gespeichert. Wir machen das selbe für die Höhe. Wir ermitteln die Bild-Höhe aus pPicture und speichern den Pixel-Wert in lHeightPixels.
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &glMaxTexDim); // ermittle maximal unterstützte Texturgröße
pPicture->get_Width(&lWidth); // ermittle IPicture Breite (konvertiere in Pixel)
lWidthPixels = MulDiv(lWidth, GetDeviceCaps(hdcTemp, LOGPIXELSX), 2540);
pPicture->get_Height(&lHeight); // ermittle IPicture Höhe (konvertiere in Pixel)
lHeightPixels = MulDiv(lHeight, GetDeviceCaps(hdcTemp, LOGPIXELSY), 2540);
Als nächstes überprüfen wir, ob die Bild-Breite in Pixeln kleiner als die maximal von der Grafikkarte unterstützte Breite ist.
Wenn die Bild-Breite in Pixel kleiner als die maximal unterstützte Breite ist, verkleinern wir das Bild zu einer Potenz von 2 basierend auf der aktuellen Bildbreite in Pixeln. Wir addieren 0.5f, so dass das Bild immer größer gemacht wird, wenn der nächste Wert größer ist. Zum Beispiel... wenn unser Bildbreite 400 wäre und die Grafikkarte eine maximale Breite von 512 unterstützen würde... wäre es besser die Breite auf 512 zu setzen. Wenn wir die Breite auf 256 setzen würden, würde das Bild viele Details verlieren.
Wenn die Bildgröße größer als die maximale unterstützte Bild-Breite und Höhe ist , setzen wir die Bildbreite auf die maximal unterstützte Texturgröße.
Wir machen das selbe für die Bildhöhe. Die finale Bildbreite und -höhe wird in lWidthPixels und lHeightPixels gespeichert.
// verändere Bildgröße zur nächsten Potenz von 2
if (lWidthPixels <= glMaxTexDim) // Ist Bildbreite kleiner oder gleich des Limits der Karte
lWidthPixels = 1 // ansonsten setze Breite auf "Max. Potenz von zwei", die die Karte händeln kann
lWidthPixels = glMaxTexDim;
if (lHeightPixels <= glMaxTexDim) // Ist Bildhöhe größer als das Limit der Karte
lHeightPixels = 1 // ansonsten setze Höhe auf "Max. Potenz von zwei", die die Karte händeln kann
lHeightPixels = glMaxTexDim;
Nun da wir die Bilddaten geladen haben und die Höhe und Breite kennen, die das Bild haben soll, müssen wir ein temporäres Bitmap erstellen. bi wird unsere Bitmap-Header-Informationen enthalten und pBits wird die eigentlichen Bilddaten enthalten. Wir wollen das Bitmap als ein 32 Bit Bitmap erstellen, mit einer Breite von lWidthPixels und einer Höhe von lHeightPixels. Wir wollen, dass die Bild-Codierung RGB ist und das Bild wird nur eine Bitplane haben.
// erzeuge ein temporäres Bitmap
BITMAPINFO bi = {0}; // Die Art des Bitmap, dass wir haben wollen
DWORD *pBits = 0; // Zeiger auf die Bitmap Bits
bi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); // Setze Struktur Größe
bi.bmiHeader.biBitCount = 32; // 32 Bit
bi.bmiHeader.biWidth = lWidthPixels; // Zweier-Potenz Breite
bi.bmiHeader.biHeight = lHeightPixels; // Mache das Bild von oben nach unten (Positive Y-Achse)
bi.bmiHeader.biCompression = BI_RGB; // RGB Codierung
bi.bmiHeader.biPlanes = 1; // 1 Bitplane
Aus dem MSDN: Die CreateDIBSection Funktion erzeugt ein DIB, welches Applikationen direkt schreiben können. Die Funktion gibt Ihnen einen Zeiger auf den Ort der Bitmap Bit Werte. Sie können das System den Speicher für das Bitmap allozieren lassen.
hdcTemp ist unser temporärer Device Context. bi enthält unsere Bitmap Info Daten (Header Informationen). DIB_RGB_COLORS teilt dem Programm mit, dass wir RGB Daten speichern wollen, die nicht ein logische Palette indizieren (jeder Pixel wir einen Rot, Grün und Blau Wert haben).
In pBits werden die Bilddaten gespeichert (zeigt auf die Bilddaten). Die letzten beiden Parameter können ignoriert werden.
Wenn das Programm aus irgend einem Grund kein temporäres Bitmap erzeugen konnte, geben wir alles wieder frei und geben false zurück (was das Programm beendet).
Wenn die Dinge so verlaufen, wie geplant, erhalten wir ein temporäres Bitmap. Wir benutzen SelectObject um das Bitmap mit dem temporären Device Context zu verbinden.
// ein Bitmap auf diese Art zu erzeugen, erlaubt es uns, um die Farbtiefe anzugeben und gibt uns unmittelbaren Zugriff auf die Bits
hbmpTemp = CreateDIBSection(hdcTemp, &bi, DIB_RGB_COLORS, (void**)&pBits, 0, 0);
if(!hbmpTemp) // schlug Erzeugung fehl?
{
DeleteDC(hdcTemp); // lösche den Device Context
pPicture->Release(); // Dekrementiere IPicture Referenz Zähler
return FALSE; // gebe False zurück(Fehlschlag)
}
SelectObject(hdcTemp, hbmpTemp); // wähle Handle auf unseren temporären DC und unser temporäres Bitmap Objekt aus
Nun müssen wir unser temporäres Bitmap mit den Daten aus unserem Bild laden. pPicture->Render wird das für uns machen. Damit wird das BIld auch auf jede Größe angepasst, die wir haben wollen (in diesem Fall... lWidthPixels mal lHeightPixels).
hdcTemp ist unser temporärer Device Context. Die ersten beiden Parameter nach hdcTemp sind der horizontal und vertikale Offset (die anzahl der 'leeren' Pixel nach links und von oben aus gesehen). Wir wollen, dass das Bild das gesamte Bitmap ausfüllt, weshalb wir 0 für den horizontalen und 0 für den vertikalen Offset angeben.
Der vierte Parameter ist unsere horizontale Dimension des Ziel-Bitmaps und der fünfte Parameter ist die vertikale Dimension. Diese Parameter kontrollieren wie viel das Bild gezerrt oder gestaucht wird, um in die Dimension zu passen, die wir haben wollen.
Der nächste Parameter (0) ist ist der horizontale Offset von dem wir die Quell-Daten lesen wollen. Wir zeichnen von links nach rechts, so dass der Offset 0 ist. Das macht Sinn, sobald Sie sehen, was wir mit dem vertikalen Offset machen (hoffentlich).
Der lHeight Parameter ist der vertikale Offset. Wir wollen die Daten von unten aus dem Quell-Bild nach oben lesen. Indem wir einen Offset gleich lHeight verwenden, bewegen wir uns nach ganz unten, im Quell-Bild.
lWidth ist die Menge die aus dem Quell-Bild zu kopieren ist. Wir wollen alle horizontalen Daten aus dem Quell-Bild kopieren. lWidth enthält alle Daten von links nach rechts.
Der zweiletzte Parameter ist etwas anders. Es ist ein negativer Wert. Ein negatives lHeight um genau zu sein. Das bedeutet, dass wir alle Daten vertikal kopieren wollen, aber wir wollen mit dem Kopieren von unten nach oben verfahren. Auf diese Weise wird das Bild geflippt, wenn es in das Ziel-Bitmap kopiert wird.
Der letzte Parameter wird nicht verwendet.
// Render das IPicture auf das Bitmap
pPicture->Render(hdcTemp, 0, 0, lWidthPixels, lHeightPixels, 0, lHeight, lWidth, -lHeight, 0);
So, nun haben wir ein neues Bitmap mit einer Breite von lWidthPixels und einer Höhe von lHeightPixels. Das neue Bitmap wurde gleichzeitig richtig geflippt.
Unglücklicherweise sind die Daten im BGR Format gespeichert. Deshalb müssen wir die Rot und Blau Pixel vertauschen, um aus dem Bitmap ein RGB Bild zu machen. Zur selben Zeit setzen wir den Alpha-Wert auf 255. Sie können diesen Wert beliebig ändern. Dieses Demo verwendet Alpha nicht, weshalb es in diesem Tutorial egal ist!
// konvertiere vom BGR zum RGB Format und füge einen Alpha Wert von 255 hinzu
for(long i = 0; i <lWidthPixels * lHeightPixels; i++) // iteriere durch alle Pixel
{
BYTE* pPixel = (BYTE*)(&pBits[i]); // hole den aktuellen Pixel
BYTE temp = pPixel[0]; // speichere erste Farbe in der Temp Variable (Blau)
pPixel[0] = pPixel[2]; // verschiebe Rot-Wert an die korrekte Position (1ste)
pPixel[2] = temp; // verschiebe Temp Wert an die korrekte Blau-Position (3te)
pPixel[3] = 255; // Setze den Alpha Wert auf 255
}
Nach all dieser Arbeit haben wir ein Bitmap-Image, welches als Textur verwendet werden kann. Wir binden mit texid und generieren die Textur. Wir wollen für die beiden min und max Filter lineare Filterung verwenden (sieht gut aus).
Wir erhalten die Bilddaten aus pBits. Wenn wir die Textur generieren, benutzen wir lWidthPixels und lHeightPixels ein letztes Mal, um die Textur Breite und Höhe zu setzen.
Nachdem die 2D Textur generiert wurde, können wir aufräumen. Wir benötigen das temporäre Bitmap nicht mehr länger oder auch nicht den temporären Device Context. Beide werden gelöscht. Wir können ebenfalls pPicture freigeben... YEAH!!!
glGenTextures(1, &texid); // erzeuge die Textur
// Typische Textur Erzeugung mit Hilfe von Daten aus einem Bitmap
glBindTexture(GL_TEXTURE_2D, texid); // Binde die Textur ID
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); // (ändern Sie das hier, um die Art des Filtering zu verwenden, die Sie haben wollen)
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); // (ändern Sie das hier, um die Art des Filtering zu verwenden, die Sie haben wollen)
// (ändern Sie dies, wenn Sie Mipmaps verwenden wollen)
glTexImage2D(GL_TEXTURE_2D, 0, 3, lWidthPixels, lHeightPixels, 0, GL_RGBA, GL_UNSIGNED_BYTE, pBits);
DeleteObject(hbmpTemp); // lösche das Objekt
DeleteDC(hdcTemp); // lösche den Device Context
pPicture->Release(); // Dekrementiere IPicture Referenz Zähler
return TRUE; // gebe True zurück (Alles in Ordnung)
}
Der folgende Code überprüft, ob die Grafikkarte des Benutzers die EXT_fog_coord Extension unterstützt. Dieser Code kann NUR DANN aufgerufen werden, wenn Ihr OpenGL Programm ein Rendering Context hat. Wenn Sie versuchen, ihn vorher aufzurufen, bevor Sie das Fenster initialisiert haben, werden Sie Fehler erhalten.
Als erstes erzeugen wir einen String mit dem Namen unserer Extension.
Wir allozieren dann genügend Speicher, um die Liste der von der Grafikkarte des Benutzers unterstützeden OpenGL Extensionen aufzunehmen. Die Liste der unterstützenden Extensionen wir mit dem glGetString(GL_EXTENSIONS) Befehl ermittelt. Die zurückgelieferten Informationen werden nach glextstring kopiert.
Wenn wir erst einmal die Liste der unterstütztenden Extensionen haben, benutzen wir strstr, um zu sehen, ob unsere Extension (Extension_Name) in der Liste der unterstütztende Extensionen (glextstring) vorhanden ist.
Wenn die Extension nicht unterstützt wird, wird FALSE zurückgegeben und das Programm beendet. Wenn alles in Ordnung ist, geben wir glextstring frei (wir brauchen die Liste der unterstütztenden Extensionen nicht weiter).
int Extension_Init()
{
char Extension_Name[] = "EXT_fog_coord";
// alloziere Speicher für unseren Extension String
char* glextstring=(char *)malloc(strlen((char *)glGetString(GL_EXTENSIONS))+1);
strcpy (glextstring,(char *)glGetString(GL_EXTENSIONS)); // ermittle die Extension Liste, speichere in glextstring
if (!strstr(glextstring,Extension_Name)) // überprüfe, ob die Extension unterstützt wird
return FALSE; // wenn nicht, gebe FALSE zurück
free(glextstring); // gebe allozierten Speicher frei
Ganz am Anfang des PRogramms haben wir glFogCoordfEXT definiert. Wie dem auch sei, der Befehl wird nicht funktionieren, bis wir die Funktion mit der eigentlichen OpenGL Extension verbunden haben. Wir machen das, indem wir glFogCoordfEXT die Adresse der OpenGL Fog Extension übergeben. Wenn wir glFogCoordfEXT aufrufen, wird der eigentliche Extension Code aufgerufen und wird den Parameter erhalten, der glFogCoordfEXT übergeben wurde.
Sorry, dies ist einer der Codeschnippsel, die sehr hart in einfachen Worten zu erklären sind (zumindest für mich).
// initialisiere und aktiviere glFogCoordEXT
glFogCoordfEXT = (PFNGLFOGCOORDFEXTPROC) wglGetProcAddress("glFogCoordfEXT");
return TRUE;
}
In diesem Codeabschnitt rufen wir die Routine zur Überprüfung der unterstützten Extensionen auf, laden unsere Textur und initialisieren OpenGL.
Zur Zeit, wo wir zu diesem Code-Abschnitt kommen, hat unser Programm einen RC (Rendering Context). Das ist wichtig, da Sie einen Rendering Context haben müssen, bevor Sie überprüfen können, ob eine Extension von der Benutzer-Grafikkarte unterstützt wird.
Deshalb rufen wir Extension_Init( ) auf, um zu sehen, ob die Karte die Extension unterstützt. Wenn die Extension nicht unterstützt wird, gibt Extension_Init( ) false zurück und die Überprüfung schlägt fehl. Das würde das Programm beenden lassen. Sie könnten dann auch eine Message Box anzeigen lassen, wenn Sie wollen. Zur Zeit wird das Programm einfach nicht laufen.
Wenn die Extension unterstützt wird, versuchen wir unsere wall.bmp Textur zu laden. Die ID für diese Textur wird texture[0] sein. Wenn die Textur aus irgend einema Grund nicht geladen wird, wird das Programm beendet.
Die Initialisierung ist einfach. Wir aktivieren 2D Textur-Mapping. Wir setzen die Farbe zum löschen auf schwarz. Die Lösch-Tiefe auf 1.0f. Wir setzen Depth Testing auf weniger oder gleich und aktivieren Depth Testing. Das Shademodel wir auf Smooth Shading gesetzt und wir wählen 'nicest' für unsere Perspektiven Korrektur aus.
BOOL Initialize (GL_Window* window, Keys* keys) // Jeder GL Init Code & Benutzer Initialisierung kommt hier hin
{
g_window = window; // Fenster Werte
g_keys = keys; // Tasten Werte
// Fange mit Benutzer Initialisierung an
if (!Extension_Init()) // überprüfe und aktiviere Fog Extension, wenn diese verfügbar ist
return FALSE; // gebe False zurück, wenn die Extension nicht unterstützt wird
if (!BuildTexture("data/wall.bmp", texture[0])) // Lade die Wand-Textur
return FALSE; // gebe False zurück, wenn das Laden fehl schlug
glEnable(GL_TEXTURE_2D); // aktiviere Textur Mapping
glClearColor (0.0f, 0.0f, 0.0f, 0.5f); // schwarzer Hintergrund
glClearDepth (1.0f); // Depth Buffer Setup
glDepthFunc (GL_LEQUAL); // Die Art des Depth Testing
glEnable (GL_DEPTH_TEST); // aktiviere Depth Testing
glShadeModel (GL_SMOOTH); // wähle Smooth Shading aus
glHint (GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Setze Perspectiven Berechnung auf die Genaueste
Nun zum spaßigen Teil. Wir müssen den Nebel initialisieren. Wir fangen an, den Nebel zu aktivieren. Den Rendering Modus den wir verwenden ist linear (sieht nett aus). Die Nebelfarbe wird auf fogColor (orange) gesetzt.
Wir müssen dann die Nebel Startpoisiton setzen. Das ist der am wenigsten dichte Nebelabschnitt. Um die Dinge einfach zu halten, werden wir 1.0f als Wert für niedrigste Nebeldichte verwenden (FOG_START). Wir werden 0.0f für den dichteste Nebelabschnitt (FOG_END) nehmen.
Laut aller Dokumentationen, die ich gelesen habe, bewirkt das Setzen des Fog Hints auf GL_NICEST, dass der Nebel pro Pixel gerendert wird. Verwendet man GL_FASTEST, wird der Nebel pro Vertex gezeichnet. Ich persönlich sehe da keinen Unterschied.
Der letzte glFogi(...) Befehl teilt OpenGL mit, dass wir unseren Nebel basierend auf Vertex-Koordinaten setzen wollen. Das erlaubt es uns, unseren Nebel irgendwo in der Szene zu setzen, ohne damit die gesamte Szene zu beeinflussen (cool!).
Wir setzen den Anfangswert von camz auf -19.0f. Der eigentliche Korridor ist 30 Einheiten lang. -19.0f setzt uns also fast zum Anfang des Korridors (der Korridor wird von -15.0f bis +15.0f auf der Z-Achse gerendert).
// Set Up Fog
glEnable(GL_FOG); // aktiviere Nebel
glFogi(GL_FOG_MODE, GL_LINEAR); // Nebel Fade ist linear
glFogfv(GL_FOG_COLOR, fogColor); // Setze die Nebel-Farbe
glFogf(GL_FOG_START, 0.0f); // Setze den Nebel Start (geringste Dichte)
glFogf(GL_FOG_END, 1.0f); // Setze den Nebel Ende (dichtester Nebel)
glHint(GL_FOG_HINT, GL_NICEST); // Pro-Pixel Nebel Berechnung
glFogi(GL_FOG_COORDINATE_SOURCE_EXT, GL_FOG_COORDINATE_EXT); // Setze Nebel basierend auf den Vertice Koordinaten
camz = -19.0f; // Setze Kamera Z Position auf -19.0f
return TRUE; // gebe TRUE zurück (Initialisierung war erfolgreich)
}
Dieser Codeabschnitt wird immer aufgerufen, wenn der Benutzer das Programm verlässt. Es gibt nichts zum aufräumen, weshalb dieser Abschnitt leer bleibt!
void Deinitialize (void) // jeglich Benutzer DeInitialization kommt hier hin
{
}
Hier reagieren wir auf die Tastatur. Wie in allen vorherigen Tutorials überprüfen wir, ob die ESC Taste gedrückt wurde. Wenn ja, wird die Applikation beendet.
Wenn die F1 Taste gedrückt wurde, wechseln wir vom Fullscreen in den Fenster-Modus oder vom Fenster-Modus in den Fullscreen.
Die anderen beiden Tasten die wir überprüfen, sind die Pfeil nach oben und nach unten Tasten. Wenn die Pfeil nach oben Taste gedrückt wurde und der Wert von camz kleiner als 14.0f ist, inkrementieren wir camz. Dadurch bewegt sich der Korridor etwas näher an den Betrachter. Wenn wir über die 14.0f hinausschießen würden, würde wir direkt durch die hintere Wand laufen. Wir wollen aber nicht, dass das passiert :)
Wenn die Pfeil nach unten Taste gedrückt wurde und der Wert von camz größer als -19.0f ist, dekrementieren wir camz. Dadurch bewegen wir den Korridor etwas weiter weg vom Betrachter. Wenn wir die -19.0f überschreiten würden, wäre der Korridor zu weit im Bildschirm, so dass Sie den Eingang des Korridors sehen könnten. Auch hier... das wollen wir nicht!
Der Wert von camz wird, basierend auf der Anzahl der vergangenen Millisekunden dividiert durch 100.0f inkrementiert und dekrementiert. Das sollte das Programm dazu zwingen auf jedem Computer mit der gleichen Geschwindigkeit zu laufen.
void Update (DWORD milliseconds) // führe hier die Bewegungs-Aktualisierung durch
{
if (g_keys->keyDown [VK_ESCAPE]) // wurde ESC gedrückt?
TerminateApplication (g_window); // Terminiere das Programm
if (g_keys->keyDown [VK_F1]) // wurde F1 gedrückt?
ToggleFullscreen (g_window); // wechsel Fullscreen Modus
if (g_keys->keyDown [VK_UP] && camz// wurde die Pfeil nach oben Taste gedrückt?
camz+=(float)(milliseconds)/100.0f; // hole Objekt etwas näher ran (bewege vorwärts durch den Korridor)
if (g_keys->keyDown [VK_DOWN] && camz>-19.0f) // wurde die Pfeil nach unten Taste gedrückt?
camz-=(float)(milliseconds)/100.0f; // bewege Objekt etwas weiter weg (bewege rückwärts durch den Korridor)
}
Ich bin mir sicher, dass Sie es kaum erwarten können, den Rendering-Code zu sehen, aber wir müssen immer noch ein paar Dinge erledigen, bevor wir den Korridor zeichnen. Als erstes müssen wir den Screen und den Depth Buffer löschen. Wir resetten die Modelview Matrix und translatieren, basierend auf dem Wert, der in camz gespeichert ist, in den Screen hinein.
In dem der Wert von camz inkrementiert oder dekrementiert wird, bewegt sich der Korridor näher oder weiter weg vom Betrachter. Das vermittelt den Eindruck, dass der Betrachter sich vor oder zurück durch den Korridor bewegt... Simpel aber effektiv!
void Draw (void)
{
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // lösche Screen und Depth Buffer
glLoadIdentity (); // Resette die Modelview Matrix
glTranslatef(0.0f, 0.0f, camz); // bewege zu unserer Kamera Z Position
Die Kamera ist positioniert und nun ist es auch an der Zeit, den ersten Quad zu rendern. Das wird die HINTERE Wand sein (die Wand am Ende des Korridors).
Wir wollen, dass diese Wand am dichtesten in Nebel gehüllt ist. Wenn Sie sich den Init-Abschnitt des Codes anschauen, werden Sie sehen, dass GL_FOG_END der nebligste Abschnitt... und hat einen Wert von 1.0f.
Nebel wird genauso angewandt wir Sie Textur-Koordinaten anwenden. GL_FOG_END hat den meisten Nebel und hat einen Wert von 1.0f. Deshalb übergeben wir für unseren ersten Vertex glFogCoordfEXT einen Wert von 1.0f. Das wird dem unteren (-2.5f auf der Y-Achse) linken (-2.5f auf der X-Achse) Vertex der entferntesten Wand (die Wand die Sie am Ende des Tunnels sehen), den dichtesten Nebel (1.0f) geben.
Wir geben den anderen 3 glFogCoordfEXT Vertices ebenfalls einen Wert von 1.0f. Wir wollen das alle 4 Punkt (in der Ferne) im dichten Nebel sind.
Hoffentlich haben Sie mittlerweile Textur-Mapping Koordinaten und glVertex Koordinaten verstanden. Ich sollte das nicht noch einmal hier erklären müssen :)
glBegin(GL_QUADS); // hintere Wand
glFogCoordfEXT(1.0f); glTexCoord2f(0.0f, 0.0f); glVertex3f(-2.5f,-2.5f,-15.0f);
glFogCoordfEXT(1.0f); glTexCoord2f(1.0f, 0.0f); glVertex3f( 2.5f,-2.5f,-15.0f);
glFogCoordfEXT(1.0f); glTexCoord2f(1.0f, 1.0f); glVertex3f( 2.5f, 2.5f,-15.0f);
glFogCoordfEXT(1.0f); glTexCoord2f(0.0f, 1.0f); glVertex3f(-2.5f, 2.5f,-15.0f);
glEnd();
Nun haben wir hinten eine texturierte Wand im dichten Nebel. Nun werden wir den Boden zeichnen. Das geschieht etwas anders, aber wenn Sie das Muster erst einmal erkannt haben, ist es ganz einleuchtend!
Wie alle Quads, hat der Boden 4 Punkte. Der Y-Wert ist immer -2.5f. Der linke vertex ist -2.5f, der rechte Vertex ist 2.5f und der Boden verläuft von -15.0f auf der Z-Achse bis +15.0f auf der Z-Achse.
Wir wollen, dass der Abschnitt des Bodens, der am weitesten in der Ferne liegt, am dichtesten in Nebel gehüllt ist. Deshalb weisen wir diesen glFogCoordfEXT Vertices erneut einen Wert von 1.0f zu. BEachten Sie, dass jeder Vertex, der bei -15.0f gezeichnet wird, einen glFogCoordfEXT Wert von 1.0f hat...?
Die Teile des Bodens, die am nähsten zum Betrachter sind (+15.0f) werden am wenigsten Nebel haben. GL_START_FOG ist der Wert, der den wenigsten Nebel repräsentiert und ist gleich 0.0f. Deshalb weisen wir diesen Punkten einen glFogCoordfEXT Wert von 0.0f zu.
Was Sie sehen sollten, wenn Sie das Programm laufen lassen, ist wirklich dicker Nebel am Boden am hinteren Ende und leichter Nebel in der Nähe ganz oben. Der Nebel ist nicht dicht genug, um den gesamten Korridor zu füllen. Eigentlich verzieht er sich auf halber Strecke den Korridor hinunter, obwohl GL_START_FOG gleich 0.0f ist.
glBegin(GL_QUADS); // Boden
glFogCoordfEXT(1.0f); glTexCoord2f(0.0f, 0.0f); glVertex3f(-2.5f,-2.5f,-15.0f);
glFogCoordfEXT(1.0f); glTexCoord2f(1.0f, 0.0f); glVertex3f( 2.5f,-2.5f,-15.0f);
glFogCoordfEXT(0.0f); glTexCoord2f(1.0f, 1.0f); glVertex3f( 2.5f,-2.5f, 15.0f);
glFogCoordfEXT(0.0f); glTexCoord2f(0.0f, 1.0f); glVertex3f(-2.5f,-2.5f, 15.0f);
glEnd();
Die Decke wir genauso gezeichnet, wie der Boden gezeichnet wurde, mit dem kleinen Unterschied, dass die Decke bei 2.5f auf der Y-Achse gezeichnet wird.
glBegin(GL_QUADS); // Decke
glFogCoordfEXT(1.0f); glTexCoord2f(0.0f, 0.0f); glVertex3f(-2.5f, 2.5f,-15.0f);
glFogCoordfEXT(1.0f); glTexCoord2f(1.0f, 0.0f); glVertex3f( 2.5f, 2.5f,-15.0f);
glFogCoordfEXT(0.0f); glTexCoord2f(1.0f, 1.0f); glVertex3f( 2.5f, 2.5f, 15.0f);
glFogCoordfEXT(0.0f); glTexCoord2f(0.0f, 1.0f); glVertex3f(-2.5f, 2.5f, 15.0f);
glEnd();
Die rechte Wand wird ebenfalls so gezeichnet. Außer, dass die X-Achse immer 2.5f ist. Der entfernteste Punkt auf der Z-Achse ist immer noch auf glFogCoordfEXT(1.0f) gesetzt und der nähste Punkt auf der Z-Achse ist immer noch glFogCoordfEXT(0.0f).
glBegin(GL_QUADS); // rechte Wand
glFogCoordfEXT(0.0f); glTexCoord2f(0.0f, 0.0f); glVertex3f( 2.5f,-2.5f, 15.0f);
glFogCoordfEXT(0.0f); glTexCoord2f(0.0f, 1.0f); glVertex3f( 2.5f, 2.5f, 15.0f);
glFogCoordfEXT(1.0f); glTexCoord2f(1.0f, 1.0f); glVertex3f( 2.5f, 2.5f,-15.0f);
glFogCoordfEXT(1.0f); glTexCoord2f(1.0f, 0.0f); glVertex3f( 2.5f,-2.5f,-15.0f);
glEnd();
Hoffentlich haben Sie nun verstanden, wie die Dinge funktionieren. Alles in der Ferne wird mehr Nebel haben und sollte auf einen Wert von 1.0f gesetzt werden. Alles was Nah ist, sollte auf 0.0f gesetzt werden.
Natürlich können Sie immer mit den GL_FOG_START und GL_FOG_END Werten herumspielen, um zu sehen, wie sie die Szene beeinflussen.
Der Effekt sieht nicht gerade überzeugend aus, wenn Sie den Start und End Wert vertauschen. Die Illusion wird von der hinteren Mauer erzeugt, die komplett orange ist! Der Effekt sieht am besten in Korridoren oder Ecken aus, wo der Spieler nicht in eine andere Richtung als den Nebel schauen kann!
Diese Art von Nebel-Effekt funktioniert am besten, wenn der Spieler in den nebligen Raum schauen kann, aber nicht wirklich hineingehen kann. Ein gutes Beispiel wäre eine tiefe Höhle, zusammen mit ein paar knarrenden Geräuschen. Der Spieler könnte in die Höhle schauen, aber könnte nicht hinein gehen.
glBegin(GL_QUADS); // linke Wand
glFogCoordfEXT(0.0f); glTexCoord2f(0.0f, 0.0f); glVertex3f(-2.5f,-2.5f, 15.0f);
glFogCoordfEXT(0.0f); glTexCoord2f(0.0f, 1.0f); glVertex3f(-2.5f, 2.5f, 15.0f);
glFogCoordfEXT(1.0f); glTexCoord2f(1.0f, 1.0f); glVertex3f(-2.5f, 2.5f,-15.0f);
glFogCoordfEXT(1.0f); glTexCoord2f(1.0f, 0.0f); glVertex3f(-2.5f,-2.5f,-15.0f);
glEnd();
glFlush (); // Flushe die GL Rendering Pipeline
}
Ich hoffe wirklich, dass Sie dieses Tutorial genossen haben. Es wurde über einen Zeitraum von 3 Tagen geschrieben... 4 Stunden pro Tag. Die meiste Zeit ist dabei für das Schreiben des Textes draufgegangen, den Sie gerade lesen.
Ich wollte einen 3D Raum mit Nebel in einer Ecke des Raumes machen. Unglücklicherweise hatte ich aber nur recht wenig Zeit, um an dem Code zu arbeiten.
Selbst wenn der Gang in diesem Tutorial recht einfach ist, ist der eigentliche Nebel-Effekte doch recht cool! Das Modifizieren des Codes für Ihre eigenen Projekte sollte nur recht wenig Aufwand mit sich bringen.
Dieses Tutorial zeigt Ihnen wie Sie glFogCoordfEXT benutzen können. Es ist schnell, sieht gut aus und ist sehr einfach zu bedienen! Es ist wichtig anzumerken, dass das nur EIN Weg von vielen verschiedenen Wegen ist, um volumetrischen Nebel zu erzeugen. Der selbe Effekt könnte erzeugt werden, indem man Blending, Partikel, Masken, etc. verwendet.
Wie immer... wenn Sie Fehler in diesem Tutorial finden, lassen Sie es mich wissen. Wenn Sie denken, dass Sie einen Codeabschnitt besser beschreiben können (meine Ausdrucksweise ist nicht immer ganz klar), senden Sie mir eine E-Mail!
Ein großer Teil des Textes wurde letzte Nacht geschrieben und selbst wenn es keine Entschuldigung ist, wird mein Schreiben immer schlimmer mit zunehmender Müdigkeit. Senden Sie mir bitte eine E-Mail wenn Sie doppelte Worte, Rechtschreibfehler, etc. finden.
Die ursprüngliche Idee zu diesem Tutorial, wurde mir vor langer Zeit geschickt. Danach habe ich die original Email verloren. Der Person, die diese Idee eingesandt hat... Dankeschön!
Jeff Molofee (NeHe)
* DOWNLOAD Visual C++ Code für diese Lektion.
* DOWNLOAD Borland C++ Builder 6 Code für diese Lektion. ( Conversion by Le Thanh Cong )
* 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 Rob Dieffenbach )
* DOWNLOAD Linux/SDL Code für diese Lektion. ( Conversion by Anthony Whitehead )
* DOWNLOAD Python Code für diese Lektion. ( Conversion by Brian Leair )
* DOWNLOAD Visual Studio .NET Code für diese Lektion. ( Conversion by Joachim Rohde )
Deutsche Übersetzung: Joachim Rohde
Der original Text ist hier zu finden.
Die original OpenGL Tutorials stammen von NeHe's Seite.