www.codeworx.org/opengl-tutorials/Tutorial 10: Dynamische 3D Welten

Lektion 10: Dynamische 3D Welten

Das Tutorial wurde erstellt von Lionel Brits. Es beschreibt nur die Passagen des Codes die hinzugefügt wurden. Das Programm wird durch einfaches kopieren der Zeilen nicht laufen. Wenn du wissen willst wo genau welche Zeilen eingefügt werden müssen, lade dir den Source runter und vergleiche ihn mit diesem Tutorial.

Willkommen zum berüchtigtem Tutorial #10. Bis jetzt habt ihr schon einen sich drehenden Würfel, ein paar Sternchen und ein ungefähres Gefühl für 3D-Programmierung. Aber wartet ! Fangt nicht gleich an Quake IV zu coden. Sich drehende Würfel geben schlechte Deathmatch Gegner :-) Heutzutage brauch man schon eine große, komplizierte dynamsche 3D Welt, völlige Bewegungfreiheit und tollen Effekten wie Spiegel, Portale, Warps und natürlich hohe Frameraten. Dieses Tutorial erklärt die Grundstrukturen einer 3D Welt und wie man sich in ihr bewegt.

Datenstruktur

Es ist völlig in Ordnung seine 3D Umgebung als eine lange Reihe von Zahlen zu programmieren, aber dies wird unglaublich schwierig destso komplexer die Umgebung wird. Deshalb stellen wir unsere Daten in übersichtlicheren Gruppen zusammen. Am Anfang davon steht ein Sektor. Jede 3D Welt besteht aus einer Ansammlung von Sektoren. Ein Sector kann ein Raum, ein Würfel oder irgendein anderer Körper sein.

typedef struct tagSECTOR  // Build Our Sector Structure
{
  int numtriangles;       // Number Of Triangles In Sector
  TRIANGLE* triangle;     // Pointer To Array Of Triangles
} SECTOR;                 // Call It SECTOR
Ein Sektor enthält eine Vielzahl von Polygonen, also entwerfen wir als nächste Kategorie ein Dreieck (wir beschränken uns erstmal auf Dreiecke, sie sind um ein vielfaches leichter zu programmieren).
typedef struct tagTRIANGLE  // Build Our Triangle Structure
{
  VERTEX vertex[3];         // Array Of Three Vertices
} TRIANGLE;                 // Call It TRIANGLE

Ein Dreieck is ein Polygon was aus Schnittpunkten besteht, was uns zu unserer letzten Kategorie bringt. Ein Schnittpunkt enthält alle Daten die OpenGL braucht. Wir benötigen für jeden Punkt des Dreiecks seine Position im 3D-Raum (x, y, z) und die passenden Texturkoordinaten.
typedef struct tagVERTEX  // Build Our Vertex Structure
{
        float x, y, z;    // 3D Coordinates
        float u, v;       // Texture Coordinates
} VERTEX;                 // Call It VERTEX

Das Laden von Dateien

Das speichern der Daten in unserem Programm würde es langweilig und nicht statisch machen. Deshalb laden wir unsere Welt von der Festplatte, dies bringt mehr Flexibiltät und ausserdem kann man gleich mehrere Welten testen ohne das Programm neu zu compilieren. Ein weiterer Vorteil ist, dass der Benutzer die Welten austauschen oder verändern kann ohne wissen zu müssen wie das Programm genau funktioniert. Wir werden alles in einer TXT-Datei speichern. Das macht es uns leichter die Welt zu erstellen und zu programmieren. Die binären Dateien heben wir uns für später auf.

Die Frage ist: Wie bekommen wir die benötigten Daten aus der Datei ? Wir erstellen erstmal die Funktion SetupWorld(). Unsere Datei wird als filein definiert und wir öffnen sie nur mit Lese-Zugriff. Wenn wir fertig sind müssen wir die Datei natürlich auch wieder schliessen. Mal schaun wie es bis jetzt aussieht.



// Previous Declaration: char* worldfile = "data\\world.txt";
void SetupWorld()                  // Setup Our World
{
  FILE *filein;                    // File To Work With
  filein = fopen(worldfile, "rt"); // Open Our File

  ...
  (read our data)
  ...

  fclose(filein);                  // Close Our File
  return;                          // Jump Back
}
Die nächste Herrausforderung ist es jede einzelne Textzeile in eine Variable einzulesen. Dies kann auf verschiedene Arten erreicht werden. Ein Problem besteht darin, dass nicht alle Zeilen wichtige Informationen beinhalten werden. Leere Zeilen oder Kommentare sollten nicht gelesen werden. Lass uns also zunächst einmal die Funktion readstr() erstellen. Diese Funktion liest eine (für uns wichtige) Textzeile in einen String. Hier der Code:

void readstr(FILE *f,char *string)  // Read In A String
{
  do                                // Start A Loop
  {
    fgets(string, 255, f);          // Read One Line
  } while ((string[0] == '/') || (string[0] == '\n'));
  // See If It Is Worthy Of Processing
  
  return;                           // Jump Back
}
Als nächstes müssen wir die Sektordaten einlesen. Dieses Tutorial behandelt nur den Umgang mit einem Sektor. aber es ist einfach mehrere Sektoren zu implementieren. Nun aber zurück zu unserer Funktion SetupWorld(). Unser Programm muss wissen aus wievielen Dreiecken unser Sektor besteht. In unserer Textdatei definieren wir die Anzahl der Dreiecke wie folgt:
NUMPOLLIES n
Nun der Code um die Anzahl zu lesen:
int numtriangles;        // Number Of Triangles In Sector
char oneline[255];       // String To Store Data In
...
readstr(filein,oneline); // Get Single Line Of Data
sscanf(oneline, "NUMPOLLIES %d\n", &numtriangles);                      
// Read In Number Of Triangles
Der Rest unseres Ladeprozesses funktioniert genauso. Wir initialisieren unseren Sektor und füllen ihn mit Daten:

// Previous Declaration: SECTOR sector1;
char oneline[255];                                                      
// String To Store Data In

int numtriangles;                                                       
// Number Of Triangles In Sector

float x, y, z, u, v;                                                   
// 3D And Texture Coordinates
...
sector1.triangle = new TRIANGLE[numtriangles];                          
// Allocate Memory For numtriangles And Set Pointer


sector1.numtriangles = numtriangles;                                    
// Define The Number Of Triangles In Sector 1

// Step Through Each Triangle In Sector
for (int triloop = 0; triloop < numtriangles; triloop++)                
// Loop Through All The Triangles
{
  // Step Through Each Vertex In Triangle
  for (int vertloop = 0; vertloop < 3; vertloop++)                
  // Loop Through All The Vertices
  {
    readstr(filein,oneline);                                
    // Read String To Work With

    // Read Data Into Respective Vertex Values
    sscanf(oneline, "%f %f %f %f %f %f %f", &x, &y, &z, &u, &v);
    // Store Values Into Respective Vertices

    sector1.triangle[triloop].vertex[vertloop].x = x;       
    // Sector 1, Triangle triloop, Vertice vertloop, x Value=x
    sector1.triangle[triloop].vertex[vertloop].y = y;       
    // Sector 1, Triangle triloop, Vertice vertloop, y Value=y
    sector1.triangle[triloop].vertex[vertloop].z = z;       
    // Sector 1, Triangle triloop, Vertice vertloop, z Value=z
    sector1.triangle[triloop].vertex[vertloop].u = u;       
    // Sector 1, Triangle triloop, Vertice vertloop, u Value=u
    sector1.triangle[triloop].vertex[vertloop].v = v;       
    // Sector 1, Triangle triloop, Vertice vertloop, v Value=v
  }
}
Jedes Dreieck ist wie folgt definiert:
X1 Y1 Z1 U1 V1
X2 Y2 Z2 U2 V2
X3 Y3 Z3 U3 V3

Zeichnen der 3D-Welt

Nun, da wir unseren Sektor im Speicher haben müssen wir ihn nur noch auf den Bildschirm bringen. Bis jetzt haben wir immer nur kleinere Drehungen und Bewegungen gemacht und unsere Kamera befand sich immer im Ursprungspunkt des Koordinatensystems (0,0,0). Jede gute 3D-Engine würde dem Benutzer erlauben sich völlig frei in der Welt zu bewegen, so auch unsere. Eine Möglichkeit dies zu tun ist die Kamera zu bewegen und die Umgebung relativ zur Kameraposition zu zeichnen. Dies ist langsam und schwer zu programmieren. Was wir tun werden ist das :

1. Die Kamera drehen und bewegen der Eingabe entsprechend.
2. Die Welt um den Ursprung drehen, aber in die entgegengesetzte Richtung
als die Drehung der Kamera (dies sieht so aus als hätte sich die Kamera gedreht)
3. Die Welt in die der Kamera entgegengesetzten
Richtung bewegen (dies sieht so aus
als hätte sich die Kamera bewegt)

Das alles ist sehr einfach einzufügen, beginnend mit Schritt 1 (drehen und bewegen der Kamera).


if (keys[VK_RIGHT])                                                     
// Is The Right Arrow Being Pressed?
{
  yrot -= 1.5f;                                                   
  // Rotate The Scene To The Left
}

if (keys[VK_LEFT])                                                      
// Is The Left Arrow Being Pressed?
{
  yrot += 1.5f;                                                   
  // Rotate The Scene To The Right        
}

if (keys[VK_UP])                                                        
// Is The Up Arrow Being Pressed?
{
  xpos -= (float)sin(yrot*piover180) * 0.05f;                     
  // Move On The X-Plane Based On Player Direction

  zpos -= (float)cos(yrot*piover180) * 0.05f;                     
  // Move On The Z-Plane Based On Player Direction

  if (walkbiasangle >= 359.0f)                                    
  // Is walkbiasangle>=359?
  {
    walkbiasangle = 0.0f;                                   
    // Make walkbiasangle Equal 0
  }
  else                                                            
  // Otherwise
  {
    walkbiasangle+= 10;                                    
    // If walkbiasangle < 359 Increase It By 10
  }
  walkbias = (float)sin(walkbiasangle * piover180)/20.0f;         
  // Causes The Player To Bounce
}

if (keys[VK_DOWN])                                                      
// Is The Down Arrow Being Pressed?
{
  xpos += (float)sin(yrot*piover180) * 0.05f;                     
  // Move On The X-Plane Based On Player Direction

  zpos += (float)cos(yrot*piover180) * 0.05f;                     
  // Move On The Z-Plane Based On Player Direction

  if (walkbiasangle <= 1.0f)                                      
  // Is walkbiasangle<=1?
  {
    walkbiasangle = 359.0f;                                 
    // Make walkbiasangle Equal 359
  }
  else                                                            
  // Otherwise
  {
    walkbiasangle-= 10;
    // If walkbiasangle > 1 Decrease It By 10
  }
  walkbias = (float)sin(walkbiasangle * piover180)/20.0f;         
  // Causes The Player To Bounce
}

Das war doch einfach, oder ? Wenn entweder links oder rechts gedrückt wurde, wird die Variable yrot dementsprechend erhöht oder erniedrigt. Wenn vorwärts oder rückwärts gedrückt wurde wird mit Hilfe der Sinus und Kosinus Funktionen (Trigonometrie) die neue Position der Kamera berechnet. Piover180 ist einfach nur der Umrechnungsfaktor zwischen Grad und Radiant.

Als nächstes werdet ihr fragen: "Was ist dieses walkbias ?" Es ist einfach ein Wort das ich erfunden habe :-) Es ist im wesentlichen ein Effekt der auftritt wenn eine Person läuft. Der Kopf dieser Person bewegt sich sanft auf und ab. Es verändert also nur die Y Position um eine kleine Sinuskurve. Ohne dies sähe die Bewegung zu langweilig aus.

Nachdem wir das nun geklärt hätten fahren wir fort mit Schritt 2 und 3. Das passiert in der ZeichenRoutine, da unser Programm noch so einfach ist, dass es dafür keine eigene Funktion braucht.


int DrawGLScene(GLvoid)                                                 
// Draw The OpenGL Scene
{
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);             
  // Clear Screen And Depth Buffer
  glLoadIdentity();                                               
  // Reset The Current Matrix

  GLfloat x_m, y_m, z_m, u_m, v_m;                                
  // Floating Point For Temp X, Y, Z, U And V Vertices
  GLfloat xtrans = -xpos;                                         
  // Used For Player Translation On The X Axis
  GLfloat ztrans = -zpos;                                         
  // Used For Player Translation On The Z Axis
  GLfloat ytrans = -walkbias-0.25f;                               
  // Used For Bouncing Motion Up And Down
  GLfloat sceneroty = 360.0f - yrot;                              
  // 360 Degree Angle For Player Direction

  int numtriangles;                                               
  // Integer To Hold The Number Of Triangles

  glRotatef(lookupdown,1.0f,0,0);                                 
  // Rotate Up And Down To Look Up And Down
  glRotatef(sceneroty,0,1.0f,0);                                  
  // Rotate Depending On Direction Player Is Facing
        
  glTranslatef(xtrans, ytrans, ztrans);                           
  // Translate The Scene Based On Player Position
  glBindTexture(GL_TEXTURE_2D, texture[filter]);                  
  // Select A Texture Based On filter
        
  numtriangles = sector1.numtriangles;                            
  // Get The Number Of Triangles In Sector 1
        
  // Process Each Triangle
  for (int loop_m = 0; loop_m < numtriangles; loop_m++)           
  // Loop Through All The Triangles
  {
  glBegin(GL_TRIANGLES);                                  
    // Start Drawing Triangles
    glNormal3f( 0.0f, 0.0f, 1.0f);                  
    // Normal Pointing Forward
    x_m = sector1.triangle[loop_m].vertex[0].x;     
    // X Vertex Of 1st Point
    y_m = sector1.triangle[loop_m].vertex[0].y;     
    // Y Vertex Of 1st Point
    z_m = sector1.triangle[loop_m].vertex[0].z;     
    // Z Vertex Of 1st Point
    u_m = sector1.triangle[loop_m].vertex[0].u;     
    // U Texture Coord Of 1st Point
    v_m = sector1.triangle[loop_m].vertex[0].v;     
    // V Texture Coord Of 1st Point
    glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m); 
    // Set The TexCoord And Vertice
                        
    x_m = sector1.triangle[loop_m].vertex[1].x;     
    // X Vertex Of 2nd Point
    y_m = sector1.triangle[loop_m].vertex[1].y;     
    // Y Vertex Of 2nd Point
    z_m = sector1.triangle[loop_m].vertex[1].z;     
    // Z Vertex Of 2nd Point
    u_m = sector1.triangle[loop_m].vertex[1].u;     
    // U Texture Coord Of 2nd Point
    v_m = sector1.triangle[loop_m].vertex[1].v;     
    // V Texture Coord Of 2nd Point
    glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m); 
    // Set The TexCoord And Vertice
                        
    x_m = sector1.triangle[loop_m].vertex[2].x;     
    // X Vertex Of 3rd Point
    y_m = sector1.triangle[loop_m].vertex[2].y;     
    // Y Vertex Of 3rd Point
    z_m = sector1.triangle[loop_m].vertex[2].z;     
    // Z Vertex Of 3rd Point
    u_m = sector1.triangle[loop_m].vertex[2].u;     
    // U Texture Coord Of 3rd Point
    v_m = sector1.triangle[loop_m].vertex[2].v;     
    // V Texture Coord Of 3rd Point
    glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m); 
    // Set The TexCoord And Vertice
  glEnd();      // Done Drawing Triangles
  }
  return TRUE;  // Jump Back
}

Und voila! Wir haben unseren ersten Frame gezeichnet. Es ist nicht ganz Quake, aber hey, wir sind ja auch nicht Carmack oder Abrash. Während du das Programm testest möchtest du vielleicht F, B, PgUp oder PgDown drücken um die neuen Effekte zu sehen. PgUp/Down dreht die Kamera einfach hoch und runter. Die Textur die ich für alles verwendet habe ist eine Schlamm Textur mit einem Eindruck meines Schülerausweiss Fotos.

Jetzt denkt ihr vermutlich: "Was nun ?" Denkt nicht einmal darüber nach mit diesem Code eine Super-3D-Engine zu bauen, dafür ist er einfach nicht gemacht. man will wahrscheinlich mehr als einen Sektor im Spiel haben, besonders wenn man an die Benutzung von Portalen denkt. Ausserdem braucht man vielleicht noch Polygone die aus mehr als drei Punkten bestehen. Meine persönliche Erweiterung des Codes unterstützt bis jetzt mehrere Sektoren und zeichnet nicht mehr die Rückseite von Polygonen. Ich werde darüber auch noch ein Tutorial schreiben, da dies aber viel mathematisches Verständnis erfordert, werde ich wahrscheinlich erst ein Tutorial über Matrizen schreiben.

NeHe (05/01/00): Ich habe noch Kommentare zu jeder Zeile eingefügt. Ich hoffe das viele Sachen nun mehr Sinn ergeben und leichter verständlich sind. Vorher waren nur ein paar Zeilen kommentiert, nun sind es alle. :-)

Bitte, wenn ihr Probleme mit dem Code habt (das ist mein erstes Tutorial, deshalb sind manche Beschreibungen vielleicht etwas verwirrend), wartet nicht und schreibt mit eine mail (iam@cadvision.com)

Bis zum nächsten mal,

Lionel Brits (ßetelgeuse)
Die Source Codes und Ausführbaren Dateien zu den Kursen liegen auf der Neon Helium Website

Übersetzung von Delax & ChaosAngel/ Sundancer Inc.

(Präsentiert von www.codeworx.org)