NeHe - Lektion 10 - Eine 3D Welt laden und sich in ihr bewegen

Lektion 10



Dieses Tutorial wurde von Lionel Brits (ßetelgeuse) geschrieben. Diese Lektion erklärt nur die Code-Segmente, die hinzugefügt wurden. Nur durch hinzufügen der Zeilen wird das Programm nicht laufen. Wenn Sie daran interessiert sind, wo die Zeilen eingefügt werden müssen, laden Sie sich den Source Code herunter und folgen sie ihm, während Sie das Tutorial lesen.

Willkommen zum infamosen Tutorial 10. Bisher haben Sie einen rotierenden Würfel oder ein paar Sterne und Sie haben das grundlegende Gefühl für 3D Programmierung. Aber Moment! Laufen Sie nicht weg und fangen Sie nicht an Quake IV zu programmieren. Mit rotierende Würfel kann man einfach kein cooles Deathmatch machen :-) Was Sie heutzutage brauchen ist eine große komplizierte und dynamische 3D Welt mit 6 Winkeln an Freiheiten und atemraubende Effekte wie Spiegel, Portale, Warping und selbstverständlicherweise hohe Frame Raten. Dieses Tutorial erklärt die grundsätzlichen "Strukturen" einer 3D Welt und wie man sich in ihr bewegt.

Daten Struktur

Während es völlig in Ordnung ist, eine 3D Umgebung als eine lange Serie an Zahlen zu coden, wird es immer schwieriger, so bald die Komplexität der Umgebung wächst. Aus diesem Grund, müssen wir unsere Daten kategorizieren und zwar in ein einfacher zu handhabenden Weg. Am Anfang unserer Liste ist der Sektor. Jede 3D Welt basiert auf einer Ansammlung von Sektoren. Ein Sektor kann ein Raum, ein Würfel oder jedes andere geschlossene Volumen sein.

typedef struct tagSECTOR                        // erzeuge unsere Sektor-Struktur
{
    int numtriangles;                        // Anzahl der Dreiecke im Sektor
    TRIANGLE* triangle;                        // Zeiger auf ein Array aus Dreiecken
} SECTOR;                                // Nenne sie SECTOR

Ein Sektor enthält eine Reihe von Polygonen, weshalb die nächste Kategorie das Dreieck sein wird (wir bleiben bei Dreiecken zunächst, weil diese viel einfache zu programmieren sind.)

typedef struct tagTRIANGLE                        // erzeuge unsere Dreiecks-Struktur
{
    VERTEX vertex[3];                        // Array aus drei Vertices
} TRIANGLE;                                // Nenne sie TRIANGLE

Das Dreieck ist grundlegend ein Polygpn, dass aus Vertices (Plural von Vertex) besteht, welche uns zur letzten Kategorie kommen lässt. Der Vertex enthält die wirklichen Daten, in denen OpenGL interessiert ist. Wir definieren jeden Punkt eines Dreiecks mit seiner Position im 3D Raum (X, Y, Z) sowie seiner Textur-Koordinaten (u, v).

typedef struct tagVERTEX                        // erzeuge unsere Vertex Structur
{
    float x, y, z;                            // 3D Koordinaten
    float u, v;                            // Textur Koordinaten
} VERTEX;                                // Nenne sie VERTEX

Dateien laden

Unsere Welt-Daten innerhalb unseres Programms zu speichern, macht es recht statisch und langweilig. Wenn wir unsere Welt allerdings von der Platte laden, sind wir flexibler, da wir verschiedene Welten testen können, ohne unser Programm neu kompilieren zu müssen. Ein weiterer Vorteil ist, dass der Benutzer Welten austauschen und modifzieren kann, ohne das innere unseres Programms vor sich zu haben. Die Art der Daten-Datei, die wir benutzen werden, wird reiner Text sein. Das macht es leichter, die Daten zu ändern und wir benötigen weniger Code. Wir bewahren uns binäre Dateien für später auf.

Die Frage ist, wie wir unsere Daten aus unserer Datei bekommen. Als erstes erzeugen wir eine neue Funktion namens SetupWorld(). Wir definieren unsere Datei als filein und wir öffnen es für nur-lesen-Zugriffe. Wir müssen unsere Datei auch wieder schließen wenn wir fertig sind. Lassen Sie uns einen Blick auf den bisherigen Code werfen:

// Vorherige Deklaration: char* worldfile = "data\\world.txt";
void SetupWorld()                            // initialisiere unsere Welt
{
    FILE *filein;                            // Datei mit der gearbeitet werden soll
    filein = fopen(worldfile, "rt");                // öffne unsere Datei

    ...
    (lese unsere Daten)
    ...

    fclose(filein);                            // schliesse unsere Datei
    return;                                // springe zurück
}

Unsere nächste Herausforderung ist es, jede einzelne Zeile Text in eine Variable zu lesen. Das kann auf zahlreichen Wegen geschehen. Ein Problem ist, dass nicht alle Zeilen brauchbare Informationen enthalten. Leerzeilen und Kommentare sollen nicht gelesen werden. Lassen Sie uns eine Funktion names readstr() erzeugen. Diese Funktion liest eine bedeutsame Zeile Text in einen initialisierten String. Hier ist der Code:

void readstr(FILE *f,char *string)                    // lese einen String ein

{
    do                                // beginne eine Schleife
    {
        fgets(string, 255, f);                    // lese eine Zeile
    } while ((string[0] == '/') || (string[0] == '\n'));        // schaue nach, ob sie es wert ist bearbeitet zu werden
    return;                                // springe zurück
}

Als nächstes müssen wir die Sektor-Daten einlesen. Diese Lektion wird nur mit einem Sektor umgehen können, aber es ist recht einfach eine Multi-Sektor Engine zu implementieren. Kommen wir zurück zu SetupWorld(). Unser Programm muss wissen, wie viele Dreiecke sich in unserem Sektor befinden. In unserer Daten-Datei definieren wir die Anzahl der Dreiecke wie folgt:

NUMPOLLIES n

Hier ist der Code, der die Anzahl der Dreiecke liest:

int numtriangles;                            // Anzahl der Dreiecke im Sektor
char oneline[255];                            // String um Daten darin zu speichern
...
readstr(filein,oneline);                        // hole einzelne Zeile an Daten
sscanf(oneline, "NUMPOLLIES %d\n", &numtriangles);            // Lese Anzahl der Dreiecke ein

Der Rest unseres Welt-Lade-Prozesses wird den selben Prozess verwenden. Als nächstes initialisieren wir unseren Sektor und lesen einige Daten ein:

// Previous Declaration: SECTOR sector1;
char oneline[255];                            // String um Daten darin abzuspeichern
int numtriangles;                            // Anzahl der Dreiecke im Sektor
float x, y, z, u, v;                            // 3D und Textur Koordinaten
...
sector1.triangle = new TRIANGLE[numtriangles];                // Alloziiere Speicher für numtriangles und setze Pointer
sector1.numtriangles = numtriangles;                    // Definiere die Anzahl der Dreiecke in Sektor 1
// Gehe schrittweise durch jedes Dreieck im Sektor
for (int triloop = 0; triloop <numtriangles; triloop++)        // durchlaufe alle Dreiecke
{
    // durchlaufe jeden Vertex im Dreieck
    for (int vertloop = 0; vertloop <3; vertloop++)        // durchlaufe alle Vertices
    {
        readstr(filein,oneline);                // Lese String ein, mit dem gearbeitet werden soll
        // Lese Daten ein respektive Vertex Werte
        sscanf(oneline, "%f %f %f %f %f", &x, &y, &z, &u, &v);
        // Speicher Werte in den entsprechenden Vertices
        sector1.triangle[triloop].vertex[vertloop].x = x;    // Sektor 1, Dreieck triloop, Vertice vertloop, x Wert=x
        sector1.triangle[triloop].vertex[vertloop].y = y;    // Sektor 1, Dreieck triloop, Vertice vertloop, y Wert=y
        sector1.triangle[triloop].vertex[vertloop].z = z;    // Sektor 1, Dreieck triloop, Vertice vertloop, z Wert=z
        sector1.triangle[triloop].vertex[vertloop].u = u;    // Sektor 1, Dreieck triloop, Vertice vertloop, u Wert=u
        sector1.triangle[triloop].vertex[vertloop].v = v;    // Sektor 1, Dreieck triloop, Vertice vertloop, v Wert=v
    }
}

Jedes Dreieck in unserer Daten-Datei ist wie folgt deklariert:

X1 Y1 Z1 U1 V1
X2 Y2 Z2 U2 V2
X3 Y3 Z3 U3 V3

Welten anzeigen

Nun, da wir einen Sektor in den Speicher laden können, müssen wir ihn nur noch auf dem Bildschirm anzeigen. Bis dahin haben wir einige kleine Rotationen und Translationen vorgenommen, aber unsere Kamera war immer zentriert im Ursprung (0,0,0). Jede gute 3D Engine lässt dem Benutzer die Möglichkeit herumzulaufen und die Welt zu entdecken, und so wird es unsere auch tun. Ein Weg ist, die Kamera zu bewegen und die 3D-Umgebung relativ zur Kamera-Position zu zeichnen. Das ist langsam und schwer zu programmieren. Wir werden folgendes machen:
  1. Rotiere und translatiere die Kamera-Position entsprechend der Benutzer-Befehle
  2. Rotiere die Welt um den Ursprung herum in entgegengesetzter Richtung zur Kamera-Rotation (was die Illusion aufkommen lässt, dass die Kamera rotiert)
  3. Translatiere die Welt entgegengesetzt der Kamera (wieder um die Illusion zu erzeugen, dass die Kamera bewegt wurde)
Das ist ziemlich einfach zu implementieren. Fangen wir mit dem ersten Schritt (Rotation und Translation der Kamera) an.

if (keys[VK_RIGHT])                            // Wurde die rechte Pfeil-Taste gedrückt?
{
    yrot -= 1.5f;                            // Rotiere die Szene nach links
}

if (keys[VK_LEFT])                            // Wurde die linke Pfeil-Taste gedrückt?
{
    yrot += 1.5f;                            // Rotiere die Szene nach rechts
}

if (keys[VK_UP])                            // Wurde die Pfeil-nach-oben-Taste gedrückt?
{
    xpos -= (float)sin(heading*piover180) * 0.05f;            // bewege auf der X-Ebene basierend auf der Spieler-Richtung
    zpos -= (float)cos(heading*piover180) * 0.05f;            // bewege auf der Z-Ebene basierend auf der Spieler-Richtung
    if (walkbiasangle >= 359.0f)                    // Ist walkbiasangle>=359?
    {
        walkbiasangle = 0.0f;                    // Setze walkbiasangle gleich 0
    }
    else                                // Ansonsten
    {
         walkbiasangle+= 10;                    // Wenn walkbiasangle <359 erhöhe um 10
    }
    walkbias = (float)sin(walkbiasangle * piover180)/20.0f;        // lässt den Spieler abprallen
}

if (keys[VK_DOWN])                            // Wurde die Pfeil-nach-unten-Taste gedrückt?
{
    xpos += (float)sin(heading*piover180) * 0.05f;            // bewege auf der X-Ebene basierend auf der Spieler-Richtung
    zpos += (float)cos(heading*piover180) * 0.05f;            // bewege auf der Z-Ebene basierend auf der Spieler-Richtung
    if (walkbiasangle <= 1.0f)                    // Ist walkbiasangle
    {
        walkbiasangle = 359.0f;                    // Setze walkbiasangle gleich 359
    }
    else                                // Ansonsten
    {
        walkbiasangle-= 10;                    // Wenn walkbiasangle > 1 vermindere um 10
    }
    walkbias = (float)sin(walkbiasangle * piover180)/20.0f;        // lässt den Spieler abprallen
}

Das war ziemlich einfach. Wenn entweder die linke oder rechte Cursor-Taste gedrückt wurde, wird die Rotations-Variable yrot entweder inkrementiert oder dekrementiert. Wenn die vorwärts oder rückwärts Cursor-Taste gedrückt wurde, wird eine neue Position für die Kamera berechnet, indem Sinus und Kosinus Berechnungen durchgeführt werden (etwas Trigonometrie wird hier benötigt :-). Piover180 ist einfach ein Faktor um zwischen Grad und Radians zu konvertieren.

Als nächstes werden Sie mich fragen: Was ist dieses walkbias? Das ist ein Wort, dass ich erfunden habe :-) Grundsätzlich ist es ein Offset der auftritt, wenn eine Person umherläuft (der Kopf wippt auf und ab). Es adjustiert die Y-Position der Kamera einfach mit einer Sinus-Welle. Ich musste das hinzufügen, da ein einfaches vor- und zurückbewegen nicht gut aussah.

Nun, da wir wissen, wofür die Variablen sind, können wir mit Schritt zwei und drei fortfahren. Das wird in der Anzeige-Schleife gemacht, da unser Programm nicht kompliziert genug ist, um dafür extra eine Funktion zu schreiben.

int DrawGLScene(GLvoid)                            // Zeichne die OpenGL Szene
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);        // Lösche Screen und Depth Buffer
    glLoadIdentity();                        // Resette die aktuelleMatrix
    GLfloat x_m, y_m, z_m, u_m, v_m;                // Fließkommazahlen für temporäre X, Y, Z, U und V Vertices
    GLfloat xtrans = -xpos;                        // wird benutzt für Spieler-Bewegungen auf der X-Achse
    GLfloat ztrans = -zpos;                        // wird benutzt für Spieler-Bewegungen auf der Z-Achse
    GLfloat ytrans = -walkbias-0.25f;                // wird benutzt für springende Auf- und Ab-Bewegungen
    GLfloat sceneroty = 360.0f - yrot;                // 360 Grad Winkel für die Spieler-Richtung

    int numtriangles;                        // Integer die die Anzahl der Dreiecke enthält

    glRotatef(lookupdown,1.0f,0,0);                    // Rotiere auf und ab um nach oben und unten zu sehen
    glRotatef(sceneroty,0,1.0f,0);                    // Rotiere entsprechende in die Richtung in die der Spieler schaut

    glTranslatef(xtrans, ytrans, ztrans);                // Translate die Szene basierend auf der Position des Spielers
    glBindTexture(GL_TEXTURE_2D, texture[filter]);            // Wähle eine Textur basierend auf filter aus 

    numtriangles = sector1.numtriangles;                // Ermittle die Anzahl der Dreiecke in Sektor 1

    // Bearbeite jedes Dreieck
    for (int loop_m = 0; loop_m <numtriangles; loop_m++)        // Durchlaufe alle Dreiecke
    {
        glBegin(GL_TRIANGLES);                    // Beginne Dreiecke zu zeichnen
            glNormal3f( 0.0f, 0.0f, 1.0f);            // Normale zeigt nach vorne
            x_m = sector1.triangle[loop_m].vertex[0].x;    // X Vertex des ersten Punkts
            y_m = sector1.triangle[loop_m].vertex[0].y;    // Y Vertex des ersten Punkts
            z_m = sector1.triangle[loop_m].vertex[0].z;    // Z Vertex des ersten Punkts
            u_m = sector1.triangle[loop_m].vertex[0].u;    // U Textur-Koordinate des ersten Punkts
            v_m = sector1.triangle[loop_m].vertex[0].v;    // V Textur-Koordinate des ersten Punkts
            glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);    // Setze TexCoord und Vertice

            x_m = sector1.triangle[loop_m].vertex[1].x;    // X Vertex des zweiten Punktes
            y_m = sector1.triangle[loop_m].vertex[1].y;    // Y Vertex des zweiten Punktes
            z_m = sector1.triangle[loop_m].vertex[1].z;    // Z Vertex des zweiten Punktes
            u_m = sector1.triangle[loop_m].vertex[1].u;    // U Textur-Koordinate des zweiten Punktes
            v_m = sector1.triangle[loop_m].vertex[1].v;    // V Textur-Koordinate des zweiten Punktes
            glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);    // Setze die TexCoord und Vertice

            x_m = sector1.triangle[loop_m].vertex[2].x;    // X Vertex des dritten Punktes
            y_m = sector1.triangle[loop_m].vertex[2].y;    // Y Vertex des dritten Punktes
            z_m = sector1.triangle[loop_m].vertex[2].z;    // Z Vertex des dritten Punktes
            u_m = sector1.triangle[loop_m].vertex[2].u;    // U Textur-Koordinate des dritten Punktes
            v_m = sector1.triangle[loop_m].vertex[2].v;    // V Textur-Koordinate des dritten Punktes
            glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);    // Setze die TexCoord und Vertice
        glEnd();                        // Fertig mit zeichnen der Dreiecke
    }
    return TRUE;                            // Springe zurück
}

Und voila! Wir haben unser erstes Frame gezeichnet. Das ist zwar nicht gleich Quake, aber hey, wir sind ja auch nicht Carmack oder Abrash. Während das Programm läuft, können Sie die Tasten F, B, Bild auf und Bild ab drücken, um die hinzugefügten Effekte zu sehen. Bild auf/ab lässt die Kamera einfach nach oben und unten fahren (der selbe Prozess wie wenn man sich auf der horizontalen bewegt.) Die mitgelieferte Textur ist einfach eine Schmutz-Textur mit einem Bumpmap meines Schulausweis-Bildes; jedenfalls ist es das, wenn NeHe sich entscheided, diese zu behalten :-).

So, als nächstes werden Sie wohl darüber nachdenken, was als nächstes kommt. Denken Sie nicht einmal daran, diesen Code zu benutzen um eine komplette 3D Engine zu erstellen, da er nicht dafür designt wurde. Sie wollen wahrscheinlich mehr als einen Sektor in Ihrem Spiel, vor allem wenn Sie Portale implementieren. Sie wollen wohl auch Polygone mit mehr als 3 Vertices haben, was wieder notwendig für eine Portal-Engine ist. Meine aktuelle Implementation dieses Codes erlaubt mehrere Sektoren zu laden und Backface Culling (Polygone die nicht von der Kamera erfasst werden, werden auch nicht gezeichnet). Ich werde ein Tutorial darüber schreiben, da es aber eine Menge an Mathe benötigt, werden ich erst ein Tutorial über Matrizen schreiben.

NeHe (05/01/00):

Ich habe jede Zeile in diesem Tutorial vollständig kommentiert. Hoffentlich ist es nun verständlicher. Nur ein paar Zeilen waren kommentiert, nun sind es alle :)

Wenn ihr irgendwelche Probleme mit dem Code / Tutorial habt (das ist mein erstes Tutorial, deshalb sind meine Erklärungen etwas wage), zögert bitte nicht, mir eine E-Mail zu senden (iam@cadvision.com) Bis zum nächsten Mal...

Lionel Brits (ßetelgeuse)

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 C# Code für diese Lektion. ( Conversion by Brian Holley )
* DOWNLOAD Code Warrior 5.3 Code für diese Lektion. ( Conversion by Scott Lupton )
* DOWNLOAD Cygwin Code für diese Lektion. ( Conversion by Stephan Ferraro )
* DOWNLOAD D Language Code für diese Lektion. ( Conversion by Familia Pineda Garcia )
* DOWNLOAD Delphi Code für diese Lektion. ( Conversion by Michal Tucek )
* DOWNLOAD Dev C++ Code für diese Lektion. ( Conversion by Dan )
* DOWNLOAD Euphoria Code für diese Lektion. ( Conversion by Evan Marshall )
* DOWNLOAD Game GLUT Code für diese Lektion. ( Conversion by Milikas Anastasios )
* DOWNLOAD Irix Code für diese Lektion. ( Conversion by Rob Fletcher )
* DOWNLOAD Java Code für diese Lektion. ( Conversion by Jeff Kirby )
* DOWNLOAD Jedi-SDL Code für diese Lektion. ( Conversion by Dominique Louis )
* DOWNLOAD JoGL Code für diese Lektion. ( Conversion by Nicholas Campbell )
* DOWNLOAD LCC Win32 Code für diese Lektion. ( Conversion by Robert Wishlaw )
* DOWNLOAD Linux Code für diese Lektion. ( Conversion by Richard Campbell )
* DOWNLOAD Linux/GLX Code für diese Lektion. ( Conversion by Mihael Vrbanec )
* DOWNLOAD Linux/SDL Code für diese Lektion. ( Conversion by Ti Leggett )
* DOWNLOAD LWJGL Code für diese Lektion. ( Conversion by Mark Bernard )
* DOWNLOAD Mac OS Code für diese Lektion. ( Conversion by Anthony Parker )
* DOWNLOAD Mac OS X/Cocoa Code für diese Lektion. ( Conversion by Bryan Blackburn )
* DOWNLOAD MASM Code für diese Lektion. ( Conversion by Nico (Scalp) )
* DOWNLOAD Visual C++ / OpenIL Code für diese Lektion. ( Conversion by Denton Woods )
* DOWNLOAD Power Basic Code für diese Lektion. ( Conversion by Angus Law )
* DOWNLOAD Pelles C Code für diese Lektion. ( Conversion by Pelle Orinius )
* DOWNLOAD Python Code für diese Lektion. ( Conversion by Ryan Showalter )
* DOWNLOAD Visual Basic Code für diese Lektion. ( Conversion by Jarred Capellman )
* DOWNLOAD Visual Basic Code für diese Lektion. ( Conversion by Ross Dawson )
* DOWNLOAD Visual Fortran Code für diese Lektion. ( Conversion by Jean-Philippe Perois )
* 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.