NeHe - Lektion 31 - Model Loading (Milkshape)

Lektion 31



Model Rendering Tutorial von Brett Porter (brettporter@yahoo.com)

Der Source für dieses Projekt stammt aus PortaLib3D, einer Library die ich geschrieben habe, die dem Benutzer Dinge ermöglicht, wie das Anzeigen von Modellen mit nur sehr wenig Extra-Code. Damit Sie aber einer solchen Library trauen können, sollten Sie verstehen, was diese macht und dieses Tutorial hilft Ihnen dabei.

Die Teile von ProtaLib3D die hier vorgestellt werden, unterliegen meinen Copyright-Anmerkungen. Das bedeutet nicht, dass Sie sie nicht nutzen können - es bedeutet vielmehr, dass wenn Sie den Code in Ihr eigenes Projekt hineinkopieren, dass Sie mir entsprechend Credit dafür geben müssen. Das ist alles. Wenn Sie lieber lesen, verstehen und den Code selbst re-implenetieren (und das ist es, wozu ich Sie ermutigen möchte, wenn Sie nicht gerade die Library verwenden), dann unterliegen Sie nicht dieser Verpflichtung. Seien wir ehrlich, der Code ist nichts besonderes. Ok, kommen wir zu etwas interessanterem!

OpenGL Base Code

Der OpenGL Base Code ist in Lesson32.cpp. Das meiste kam aus Lektion 6 mit ein paar kleinen Änderungen beim Laden der Texturen und dem Zeichnen. Die Änderungen werden später besprochen.

Milkshape 3D

Das Modell, das ich in diesem Beispiel verwende ist aus Milkshape 3D. Der Grund warum ich das benutze ist der, das es ein verdammt gutes Modelling Package ist und sein Datei-Format enthält, so dass es einfach zu parsen und verstehen ist. Mein nächster Plan ist es, einen Anim8or (http://www.anim8or.com) Datei-Lader zu implementieren, da es kostelos ist und selbstverständlich einen 3DS Lader.

Wie dem auch sei, das Datei-Format, dass hier ausführlich beschrieben wird, ist nicht vom hauptsächlichen Interesse, um ein Modell zu laden. Sie müssen Ihre eigenen Strukturen erzeugen, um die entsprechenden Daten zu speichern und dann die Datei in dieses einlesen. Deshalb schauen wir uns als erstes die benötigten Strukturen für ein Modell an.

Modell Daten Strukturen

Diese Modell-Daten-Strukturen kommen aus der Klasse Model in Model.h. Als erstes und wichtigstem benötigen wir Vertices:

// Vertex Struktur
struct Vertex
{
    char m_boneID;    // für Skeletal Animation
    float m_location[3];
};

// benutzte Vertices
int m_numVertices;
Vertex *m_pVertices;

Im Moment können Sie die m_boneID Variable ignorieren - das folgt in einem anderen Tutorial! Das m_location Array repräsentiert die Koordinaten des Vertex (X,Y,Z). Die zwei Variablen speichern die Anzahl der Vertices und die aktuellen Vertices in einem dynamischen Array, welches vom Loader alloziiert wird.

Als nächstes müssen wir diese Vertives zu Dreiecken gruppieren:

// Dreiecks Struktur
struct Triangle
{
    float m_vertexNormals[3][3];
    float m_s[3], m_t[3];
    int m_vertexIndices[3];
};

// benutzte Dreiecke
int m_numTriangles;
Triangle *m_pTriangles;

Nun, die 3 Vertices, die das Dreieck bilden, werden in m_vertexIndices gespeichert. Diese sind Offsets in das Array von m_pVertices. Auf diese Weise wird jeder Vertex nur einmal gespeichert, Speicher gespaart (und Berechnungen, wenn es später an Animationen geht). m_s und m_t sind die (s,t) Textur-Koordinaten für jeden der 3 Vertices. Die verwendete Textur gehört zu diesem Mesh (dazu gleich mehr). Letztendlich haben wir das m_vertexNormals Element welches den Normalenvektor zu den 3 Vertices speichert. Jeder Normalenvektor hat 3 Fließkomma Koordinaten, die den Vektor beschreiben.

Die nächste Struktur, die wir in einem Modell finden, ist ein Mesh. Ein Mesh ist eine Gruppe von Dreiecken, die alle aus dem selben Material bestehen. Alle Meshes zusammen bilden das fertige Modell. Die Mesh-Struktur sieht wie folgt aus:

// Mesh
struct Mesh
{
    int m_materialIndex;
    int m_numTriangles;
    int *m_pTriangleIndices;
};

// benutzte Meshes 
int m_numMeshes;
Mesh *m_pMeshes;

Diesmal speichert m_pTrianglIndices die Dreiecke in dem Mesh genau auf die selbe Weise, wie die Dreiecke Indicies für seine Vertices speichert. Es wird dynamisch alloziiert, da die Anzahl der Dreiecke in einem Mesh nicht vorher bekannt ist und durch m_numTriangles spezifiziert wird. Letztendlich ist m_materialIndex der Index des Materials (Textur und Beleuchtung Koeffizienten), die für das Mesh verwendet werden. Es folgt die Material-Struktur:

// Material Eigenschaften
struct Material
{
    float m_ambient[4], m_diffuse[4], m_specular[4], m_emissive[4];
    float m_shininess;
    GLuint m_texture;
    char *m_pTextureFilename;
};

// benutzte Materialen
int m_numMaterials;
Material *m_pMaterials;

Hier haben wir alle Standard Beleuchtungs Koeffizienten im selben Format wie OpenGL: Ambient, Diffus, Spiegelnd, Emissionierend und Scheinend. Wir haben des weiteren das Textur Objekt m_texture und den Dateinamen (dynamisch alloziiert) der Textur, so dass die Textur erneut geladen werden kann, wenn der OpenGL Kontext verloren geht.

Der Code - Das Modell laden

Nun weiter zum Laden des Modells. Sie werden bemerken, dass es eine rein virtuelle Funktion namens loadModelData gibt, welche den Dateinamen des Modells als Argument erwartet. Wir erzeugen eine abgeleitete Klasse, MilkshapeModel, welche diese Funktion implementiert, die die geschützten, oben genannten Daten-Strukturen füllt. Schauen wir uns die Funktion an:

bool MilkshapeModel::loadModelData( const char *filename )
{
    ifstream inputFile( filename, ios::in | ios::binary | ios::nocreate );
    if ( inputFile.fail())
        return false;    // "Konnte Modell-Datei nicht öffnen."

Als erstes wird die Datei geöffnet. Es ist eine binäre Datei, daher das ios::binary. Wenn diese nicht gefunden wurde, gibt die Funktion false zurück, was einen Fehler indiziert.

    inputFile.seekg( 0, ios::end );
    long fileSize = inputFile.tellg();
    inputFile.seekg( 0, ios::beg );

Der obige Code bestimmt die Größe der Datei in Bytes.

    byte *pBuffer = new byte[fileSize];
    inputFile.read( pBuffer, fileSize );
    inputFile.close();

Dann wird die Datei als Ganzes in einen temporären Buffer gelesen.

    const byte *pPtr = pBuffer;
    MS3DHeader *pHeader = ( MS3DHeader* )pPtr;
    pPtr += sizeof( MS3DHeader );

    if ( strncmp( pHeader->m_ID, "MS3D000000", 10 ) != 0 )
        return false;    // "Keine gültige Milkshape3D Model Datei."

    if ( pHeader->m_version <3 || pHeader->m_version > 4 )
        return false;    // "Nicht händelbare Datei-Version. Es wird nur Milkshape3D Version 1.3 und 1.4 unterstützt."

Nun wird ein Zeiger auf unsere aktuelle Position in der Datei akquiriert, pPtr. Ein Zeiger auf den Header wird gespeichert und dann wird der Zeiger hinter den Header gesetzt. Sie werden bemerken, dass verschiedene MS3D... Strukturen hier verwendet werden. Diese sind am Anfang von MilkshapeModel.cpp deklariert und kommen direkt aus der Datei-Format Spezifikation. Die Felder des Headers werden überprüft, ob wir eine gültige Datei lesen.

    int nVertices = *( word* )pPtr;
    m_numVertices = nVertices;
    m_pVertices = new Vertex[nVertices];
    pPtr += sizeof( word );

    int i;
    for ( i = 0; i <nVertices; i++ )
    {
        MS3DVertex *pVertex = ( MS3DVertex* )pPtr;
        m_pVertices[i].m_boneID = pVertex->m_boneID;
        memcpy( m_pVertices[i].m_location, pVertex->m_vertex, sizeof( float )*3 );
        pPtr += sizeof( MS3DVertex );
    }

Der obige Code liest jede Vertex-Struktur aus der Datei. Als erstes wird Speicher für die Vertices alloziiert und dann jeder aus der Datei eingelesen, so wie sich der Pointer weiterbewegt. Mehrere memcpy-Aufrufe werden in dieser Funktion verwendet, welche den Inhalt der kleinen Arrays einfach kopiert. Das m_boneID Element kann von Ihnen ignoriert werden - es ist für Skeletal Animation!

    int nTriangles = *( word* )pPtr;
    m_numTriangles = nTriangles;
    m_pTriangles = new Triangle[nTriangles];
    pPtr += sizeof( word );

    for ( i = 0; i <nTriangles; i++ )
    {
        MS3DTriangle *pTriangle = ( MS3DTriangle* )pPtr;
        int vertexIndices[3] = { pTriangle->m_vertexIndices[0], pTriangle->m_vertexIndices[1], pTriangle->m_vertexIndices[2] };
        float t[3] = { 1.0f-pTriangle->m_t[0], 1.0f-pTriangle->m_t[1], 1.0f-pTriangle->m_t[2] };
        memcpy( m_pTriangles[i].m_vertexNormals, pTriangle->m_vertexNormals, sizeof( float )*3*3 );
        memcpy( m_pTriangles[i].m_s, pTriangle->m_s, sizeof( float )*3 );
        memcpy( m_pTriangles[i].m_t, t, sizeof( float )*3 );
        memcpy( m_pTriangles[i].m_vertexIndices, vertexIndices, sizeof( int )*3 );
        pPtr += sizeof( MS3DTriangle );
    }

Wie für die Vertices, speichert dieser Teil der Funktion alle Dreiecke. Während das meiste lediglich zum Kopieren der Arrays von einer Struktur zu einer anderen dient, werden Sie den Unterschied zwischen den vertexIndices und t Arrays bemerken. In der Datei werden die Vertex Indices als ein Array von Word-Werten gespeichert, aber im Modell sind sie der Einfachheit halber int-Werte (was kein kompliziertes casten erfordert). Das konvertiert die 3 Werte also einfach in Integer. Die t Werte werden alle auf 1.0f-(original Wert) gesetzt. Der Grund dafür ist, dass OpenGL ein unten-links Koordinaten System verwendet, wohingegen Milkshape ein oben-links Koordinaten-System für seine Textur-Koordinaten verwendet. Dadurch sind die Y-Koordinaten vertauscht.

    int nGroups = *( word* )pPtr;
    m_numMeshes = nGroups;
    m_pMeshes = new Mesh[nGroups];
    pPtr += sizeof( word );
    for ( i = 0; i <nGroups; i++ )
    {
        pPtr += sizeof( byte );    // Flags
        pPtr += 32;        // Name

        word nTriangles = *( word* )pPtr;
        pPtr += sizeof( word );
        int *pTriangleIndices = new int[nTriangles];
        for ( int j = 0; j <nTriangles; j++ )
        {
            pTriangleIndices[j] = *( word* )pPtr;
            pPtr += sizeof( word );
        }

        char materialIndex = *( char* )pPtr;
        pPtr += sizeof( char );

        m_pMeshes[i].m_materialIndex = materialIndex;
        m_pMeshes[i].m_numTriangles = nTriangles;
        m_pMeshes[i].m_pTriangleIndices = pTriangleIndices;
    }

Der obige Code lädt die Mesh-Daten-Strukturen (auch Gruppen in Milkshape3D genannt). Da die Anzahl der Dreiecke von Mesh zu Mesh variiert, gibt es keine Standard Struktur zum lesen. Statt dessen, werden Sie Feld für Feld eingelesen. Der Speicher für die Dreiecks Indices werden dynamisch innerhalb des Mesh alloziiert und in einem Stück eingelesen.

    int nMaterials = *( word* )pPtr;
    m_numMaterials = nMaterials;
    m_pMaterials = new Material[nMaterials];
    pPtr += sizeof( word );
    for ( i = 0; i <nMaterials; i++ )
    {
        MS3DMaterial *pMaterial = ( MS3DMaterial* )pPtr;
        memcpy( m_pMaterials[i].m_ambient, pMaterial->m_ambient, sizeof( float )*4 );
        memcpy( m_pMaterials[i].m_diffuse, pMaterial->m_diffuse, sizeof( float )*4 );
        memcpy( m_pMaterials[i].m_specular, pMaterial->m_specular, sizeof( float )*4 );
        memcpy( m_pMaterials[i].m_emissive, pMaterial->m_emissive, sizeof( float )*4 );
        m_pMaterials[i].m_shininess = pMaterial->m_shininess;
        m_pMaterials[i].m_pTextureFilename = new char[strlen( pMaterial->m_texture )+1];
        strcpy( m_pMaterials[i].m_pTextureFilename, pMaterial->m_texture );
        pPtr += sizeof( MS3DMaterial );
    }

    reloadTextures();

Zu guter Letzt werden die Material Informationen aus dem Buffer entnommen. Das geschieht auf dem selben Weg wie oben, indem jeder Beleuchtungs Koeffizient in eine neue Struktur kopiert wird. Außerdem wird neuer Speicher für den Textur Dateinamen alloziiert und dorthin kopiert. Der letzte Aufruf von reloadTextures wird verwendet um die Textur dann zu laden und sie an ein OpenGL Textur-Objekt zu binden. Diese Funktion aus der Model Base Klasse wird später beschrieben.

    delete[] pBuffer;

    return true;
}

Das letzte Fragment gibt den temporären Buffer wieder frei, da jetzt alle Daten kopiert wurde und kehrt erfolgreich zurück.

Zu diesem Zeitpunkt sind alle geschützten Element-Variablen der Model-Klasse mit den Informationen des Modells gefüllt. Sie werden auch feststellen, dass das der einzige Code in MilkshapeModel ist, da es der einzige Code ist, der Milkshape3D-spezifisch ist. Nun, bevor das Modell gerendert werden kann, ist es notwendig, die Texturen für die einzelnen Materialien zu laden. Das wird mit folgenden Code gemacht:

void Model::reloadTextures()
{
    for ( int i = 0; i <m_numMaterials; i++ )
        if ( strlen( m_pMaterials[i].m_pTextureFilename ) > 0 )
            m_pMaterials[i].m_texture = LoadGLTexture( m_pMaterials[i].m_pTextureFilename );
        else
            m_pMaterials[i].m_texture = 0;
}

Für jedes Material, wird die Textur geladen indem eine Funktion aus NeHe's Basecode verwendet wird (leicht modifiziert gegenüber den vorherigen Versionen). Wenn der Textur Dateiname ein leerer String war, dann wurde sie nicht geladen und statt den Identifizierer des Textur-Objektes auf 0 zu setzen, indizieren wir, dass keine Textur vorhanden ist.

Der Code - Das Modell zeichnen

Nun können wir mit dem Code zum Zeichnen des Modells anfangen! Das ist nicht weiter schwierig, da wir jetzt die Daten-Strukturen im Speicher haben.

void Model::draw()
{
    GLboolean texEnabled = glIsEnabled( GL_TEXTURE_2D );

Dieser erste Teil speichert den Status von OpenGLs Textur Mapping, so dass die Funktion es nicht stört. Beachten Sie aber dennoch, dass das nicht die Material Eigenschaften auf die gleiche Weise schützt.

Nun gehen wir durch alle Meshes und zeichnen diese einzeln:

    // Zeichnen die Gruppen
    for ( int i = 0; i <m_numMeshes; i++ )
    {

m_pMeshes[i] wird verwendet um die akutelle Mesh zu referenzieren. Nun hat jede Mesh seine eigenen Material-Eigenschaften, wodurch wir den OpenGL Status dementsprechend setzen können. Wenn materialIndex des Meshes -1 ist, so gibt es kein Material für dieses Mesh und es wird mit den OpenGl-Standardwerten gezeichnet.

        int materialIndex = m_pMeshes[i].m_materialIndex;
        if ( materialIndex >= 0 )
        {
            glMaterialfv( GL_FRONT, GL_AMBIENT, m_pMaterials[materialIndex].m_ambient );
            glMaterialfv( GL_FRONT, GL_DIFFUSE, m_pMaterials[materialIndex].m_diffuse );
            glMaterialfv( GL_FRONT, GL_SPECULAR, m_pMaterials[materialIndex].m_specular );
            glMaterialfv( GL_FRONT, GL_EMISSION, m_pMaterials[materialIndex].m_emissive );
            glMaterialf( GL_FRONT, GL_SHININESS, m_pMaterials[materialIndex].m_shininess );

            if ( m_pMaterials[materialIndex].m_texture > 0 )
            {
                glBindTexture( GL_TEXTURE_2D, m_pMaterials[materialIndex].m_texture );
                glEnable( GL_TEXTURE_2D );
            }
            else
                glDisable( GL_TEXTURE_2D );
        }
        else
        {
            glDisable( GL_TEXTURE_2D );
        }

Die Materialeigenschaften werden entsprechend der Werte aus dem Modell gesetzt. Beachten Sie, dass die Textur nur gebunden und aktiviert wird, wenn sie größer als 0 ist. Wenn sie auf 0 gesetzt ist, gibt es keine Textur, wehalb Texturierung deaktiviert wird. Texturierung wird ebenfalls deaktiviert, wenn es überhaupt kein Material für die Mesh gibt.

        glBegin( GL_TRIANGLES );
        {
            for ( int j = 0; j <m_pMeshes[i].m_numTriangles; j++ )
            {
                int triangleIndex = m_pMeshes[i].m_pTriangleIndices[j];
                const Triangle* pTri = &m_pTriangles[triangleIndex];

                for ( int k = 0; k <3; k++ )
                {
                    int index = pTri->m_vertexIndices[k];

                    glNormal3fv( pTri->m_vertexNormals[k] );
                    glTexCoord2f( pTri->m_s[k], pTri->m_t[k] );
                    glVertex3fv( m_pVertices[index].m_location );
                }
            }
        }
        glEnd();
    }

Die obige Sektion rendert die Dreiecke des Modells. Es durchläuft jedes Dreieck des Mesh und zeichnet dann jeden der Vertices, inklusive der Normalen und Textur-Koordinaten. Denken Sie daran, dass jedes Dreieck in einem Mesh und ebenso jeder Vertex in einem Dreieck, in das gesamte Modell Arrays indiziert (das sind die zwei verwendeten Index-Variablen). pTri ist ein Zeiger auf das aktuelle Dreieck in dem Mesh und wird verwendet um den Code zu vereinfachen indem ihm gefolgt wird.

    if ( texEnabled )
        glEnable( GL_TEXTURE_2D );
    else
        glDisable( GL_TEXTURE_2D );
}

Dieser letzte Code-Ausschnitt setzt den Textur-Mapping-Status zurück auf seinen ursprünglichen Wert.

Der einzige sonstige Code von Interesse in der Model Klasse ist der Konstruktur und Destruktor. Diese sind selbsterklärend. Der Konstruktor initialisiert alle Elemente mit 0 (oder NULL für Zeiger) und der Destruktor löscht den dynamischen Speicher aller Modell-Strukturen. Sie sollten beachten, dass wenn Sie die loadModelData Funktion zweimal für ein Modell-Objekt aufrufen, dass Sie Speicherlecks erhalten werden. Seien Sie vorsichtig!

Das letzte Thema, dass ich hier anschneiden werde, sind die Änderungen des Base Codes, um mit der neuen Model-Klasse zu rendern und was ich plane in einem zukünfigten Tutorial über einführende Skeletal Animation zu machen.

    Model *pModel = NULL;    // enthält die Model-Daten

Am Anfang des Codes in Lesson32.cpp wird das Modell deklariert, aber nicht initialisiert. Es wird in WinMain erzeugt:

    pModel = new MilkshapeModel();
    if ( pModel->loadModelData( "data/model.ms3d" ) == false )
    {
        MessageBox( NULL, "Couldn't load the model data/model.ms3d", "Error", MB_OK | MB_ICONERROR );
        return 0;                                    // Wenn Model nicht geladen wurde, beende
    }

Das Modell wird hier erzeugt und nicht in InitGL, da InitiGL jedes Mal aufgerufen wird, wenn wir den Screen-Modus ändern (den OpenGL Kontext verlieren). Aber die Modelle müssen nicht erneut geladen werden, da die Daten ja erhalten bleiben. Was nicht intakt bleibt, sind die Texturen, die an Textur-Objekte gebunden wurden, als wir die Objekte geladen haben. Deshalb wird folgende Zeile zu InitiGL hinzugefügt:

    pModel->reloadTextures();

Darin wird LoadGLTextures nochmal aufgerufen, wie wir es brauchen. Wenn mehr als ein Modell in der Szene war, dann muss diese Funktion für jedes einzelne aufgerufen werden. Wenn Sie auf einmal weiße Objekte erhalten, dann wurden Ihre Texturen korrupiert und müssen erneut korrekt geladen werden.

Letzendlich gibt es eine neue DrawGLScene Funktion:

int DrawGLScene(GLvoid)                        // Hier kommt der ganze Zeichnen-Kram hin
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);    // Löscht den Bildschirm und den Depth-Buffer
    glLoadIdentity();                    // Resettet die Ansicht (View)
    gluLookAt( 75, 75, 75, 0, 0, 0, 0, 1, 0 );

    glRotatef(yrot,0.0f,1.0f,0.0f);

    pModel->draw();

    yrot+=1.0f;
    return TRUE;                        // weiter geht's
}

Einfach oder? Wir löschen den Farb-Buffer, setzen die Identität auf Model/View-Matrix und setzen dann eine Eye-Projektion mit glutLookAt. Wenn Sie gluLookAt noch nicht zuvor verwendet haben: grundsätzlich setzt es die Kamera an eine Position (die ersten drei Parameter), platziert der Zentrum der Szene an die Position der nächsten drei Parameter und die letzten drei Parameter beschreiben den Vektor, der nach "oben" zeigt. In diesem Fall schaeun wir von (75,75,75) auf (0,0,0) - da das Modell an (0,0,0) gezeichnet wurde, wenn Sie es nicht translatiert haben, bevor Sie gezeichnet haben - und die positive Y-Achse zeigt nach oben. Die Funktion muss als erstes aufgerufen werden und nach dem Laden der Indentitä um sich auf diese Weise zu benehmen.

Um es etwas interessanter zu machen, rotiert die Szene mittels glRotatef ständig um die Y-Achse.

Letztendlich wird das Modell mit seiner draw-Element-Funktion gezeichnet. Es wird mittig am Ursprunkt gezeichnet (davon ausgehend, dass es um den Ursprung in Milkshape 3D gemodellt wurde!), so dass Sie es mit den einfachen entsprechenden GL-Funktionen positionieren, rotieren oder skalieren können, bevor Sie es zeichnen. Voila! Um das ganze zu testen - versuchen Sie Ihre eigenen Modell in Milkshape zu machen (oder benutzen Sie die Import-Funktion) und laden Sie diese statt dessen, indem Sie einfach die Zeile in WinMain ändern. Oder fügen Sie sie der Szene hinzu und zeichnen Sie mehrere Models!

Was kommt als nächstes?

In einem zukünftigen Tutorial für NeHe Productions werde ich erklären, wie man diese Klassen-Struktur so erweitert, um sie auch für Skeletal Animationen verwenden kann. Und wenn ich dazu komme, werde ich mehr Loader-Klassen schreiben, um dieses Programm vielseitiger zu machen.

Der Schritt zu Skeletal Animation ist nicht so groß, wie es aussieht, obwohl die verwendete Mathematik viel trickreicher ist. Wenn Sie noch nicht viel von Matrizen und Vektoren verstehen, ist es jetzt Zeit, sich diese anzueignen! Es gibt genügend Material im Netz, die Ihnen da weiterhelfen können.

Bis denn!

Einige Informationen über Brett Porter: Geboren in Australien, studierte er an der Universität von Wollongong und graduiert zur Zeit in BCompSc und BMath. Er fing mit 12 Jahren an, in Basic auf einem Commodore 64 "Klon" namens VZ300, zu Programmieren, ist aber schnell zu Pascal, Intel Assembler, C++ und Java gekommen. In den letzten paar Jahren ist 3D Programmierung zu seinem Interesse geworden und OpenGL das Graphik-API seiner Wahl. Für mehr Informationen besuchen Sie seine Homepage unter: http://rsn.gamedev.net.

Ein Nachfolger Tutorial über Skeletal Animation kann auf Brett's Homapage gefunden werden. Folgen Sie diesem Link!

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 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 GLut Code für diese Lektion. ( Conversion by Rocco Balsamo )
* DOWNLOAD Linux/GLX Code für diese Lektion. ( Conversion by Rodolphe Suescun )
* 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.