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)