Lektion 34: Landschaften aus Heightmaps generieren
Willkommen zu einem neuen, hoffentlich spannenden Tutorial. Der Code dieser
Lektion stammt von Ben Humphrey und basiert größtenteils auf Lektion
1.
Dieses Tutorial wird besprechen, wie halbwegs realistische, detailreiche Landschaften
aus Heightmaps generiert werden können. Heightmaps sind Höhenwerte
einer Landschaft, die zumeist gitterartig angeordnet sind. Oberflächen
(Landschaften, Reliefe, Gesichter) lassen sich so recht einfach und sehr genau
beschreiben. Heightmaps müssen nicht zwingend aus Grafiken generiert werden,
alle möglichen Datenstrukturen sind geeignet (man denke an diverse Winamp-Plugins,
dort dienen ja Informationen aus dem Audio Stream als Grundlage oder an 3D-Simulationen
irgendwelcher physikalische Vorgänge.) Aber keine Angst, diesmal soll es
um eine einfache Landschaft gehen, die aus einer *.RAW-Grafik generiert wird.
#include <windows.h> // Header File For Windows
#include <stdio.h> // Header file For Standard Input/Output ( NEW )
#include <gl\gl.h> // Header File For The OpenGL32 Library
#include <gl\glu.h> // Header File For The GLu32 Library
#include <gl\glaux.h> // Header File For The Glaux Library
#pragma comment(lib, "opengl32.lib") // Link OpenGL32.lib
#pragma comment(lib, "glu32.lib") // Link Glu32.lib
Die üblichen Header-Dateien werden genutzt. Der untere Code enthält
neue Variablen. MAP_SIZE definiert die Dimensionen der darzustellendenen Oberfläche,
in diesem Fall 1024*1024 mögliche Teilflächen. STEP_SIZE gibt den
Detailgrad der Landschaft an, je kleiner der Wert destso mehr Quader werden
ausgegeben, die Landschaft bekommt glattere Oberflächen und das Programm
wird ressourcenhungriger. HEIGHT_RATIO skaliert die Landschaft auf der Y-Achse,
je größer der Wert wird, destso extremer werden die Abstände
zwischen hohen und tiefen Punkten auf der "Karte", Berge werden somit
höher.
bRender definiert ob die Landschaft als Gitternetz (false) oder mit gefüllten
Polygonen (true) dargestellt werden soll.
#defineMAP_SIZE1024 // Size Of Our .RAW Height Map ( NEW )
#defineSTEP_SIZE16 // Width And Height Of Each Quad ( NEW )
#defineHEIGHT_RATIO1.5f // Ratio That The Y Is Scaled According To The X And Z ( NEW )
HDChDC=NULL; // Private GDI Device Context
HGLRChRC=NULL; // Permanent Rendering Context
HWNDhWnd=NULL; // Holds Our Window Handle
HINSTANCEhInstance; // Holds The Instance Of The Application
bool keys[256]; // Array Used For The Keyboard Routine
bool active=TRUE; // Window Active Flag Set To TRUE By Default
bool fullscreen=TRUE; // Fullscreen Flag Set To TRUE By Default
bool bRender=TRUE; // Polygon Flag Set To TRUE By Default ( NEW )
Jetzt wird das für die Heightmap nötige Array definiert, in den Maßen
die mit MAP_SIZE bestimmt wurden. Das Array speichert BYTE-Werte (0-255), die
aus der RAW-Datei gelesen werden sollen. 255 ist also der größte
und 0 der kleinste Höhenwert in der Landschaft. scaleValue wird erstellt
um die gesamte Szene zu skalieren, damit der Benutzer ein- und auszoomen kann.
BYTE g_HeightMap[MAP_SIZE*MAP_SIZE];// Holds The Height Map Data ( NEW )
float scaleValue = 0.15f;// Scale Value For The Terrain ( NEW )
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);// Declaration For WndProc
GLvoid ReSizeGLScene(GLsizei width, GLsizei height)// Resize And Initialize The GL Window
{
// Hier hat sich nichts verändert.
}
Der folgende, nicht allzu komplexe Code liest die RAW-Datei ein. Die Datei
wird geöffnet und im Binärmodus gelesen ("rb"). fopen liefert
NULL zurück, falls das Öffnen nicht klappen sollte, was dann mit einer
Fehlermeldung quittiert wird.
// Loads The .RAW File And Stores It In pHeightMap
void LoadRawFile(LPSTR strName, int nSize, BYTE *pHeightMap)
{
FILE *pFile = NULL;
// Open The File In Read / Binary Mode.
pFile = fopen( strName, "rb" );
// Check To See If We Found The File And Could Open It
if ( pFile == NULL )
{
// Display Error Message And Stop The Function
MessageBox(NULL, "Can't Find The Height Map!", "Error", MB_OK);
return;
}
Die Daten werden mit fread( pHeightMap, 1, nSize, pFile ); gelesen und in pHeightMap
gespeichert. "1" gibt an, das genau 1 Byte auf einmal gelesen werden
soll, nSize wurde der Funktion LoadRawFile übergeben und hat den Wert MAP_SIZE*MAP_SIZE.
pFile ist der Zeiger auf die geöffnete Datei. Sollten beim Lesen Fehler
aufgetreten sein, wird result > 0 und eine Fehlernachricht wird ausgegeben.
fclose(pFile) schließt die Datei.
// Here We Load The .RAW File Into Our pHeightMap Data Array
// We Are Only Reading In '1', And The Size Is (Width * Height)
fread( pHeightMap, 1, nSize, pFile );
// After We Read The Data, It's A Good Idea To Check If Everything Read Fine
int result = ferror( pFile );
// Check If We Received An Error
if (result)
{
MessageBox(NULL, "Failed To Get Data!", "Error", MB_OK);
}
// Close The File
fclose(pFile);
}
Die Initialisierung verläuft wie immer. LoadRawFile wird wie oben besprochen
mit Dateinamen, der Größe der zu erstellenden Karte und dem HeightMap-Array
aufgerufen.
int InitGL(GLvoid)// All Setup For OpenGL Goes Here
{
glShadeModel(GL_SMOOTH); // Enable Smooth Shading
glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Black Background
glClearDepth(1.0f); // Depth Buffer Setup
glEnable(GL_DEPTH_TEST); // Enables Depth Testing
glDepthFunc(GL_LEQUAL); // The Type Of Depth Testing To Do
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Really Nice Perspective Calculations
// Here we read read in the height map from the .raw file and put it in our
// g_HeightMap array. We also pass in the size of the .raw file (1024).
LoadRawFile("Data/Terrain.raw", MAP_SIZE * MAP_SIZE, g_HeightMap);// ( NEW )
return TRUE;// Initialization Went OK
}
Die Funktion Height wird ausgeführt um die Höhe eines Punktes an
der Stelle (X, Y) in der Heightmap zu ermitteln. Immer wenn Arrays genutzt werden,
ist es ratsam, nicht über deren Grenzen hinnaus Daten zu lesen oder schreiben,
da dies merkwürdige Ergebnisse erzeugen wird und Abstürze provoziert.
Verhindert werden alle Fehlzugriffe mit % MAP_SIZE, da hier zu kleine oder zu
große Argumente in gültige Werte umgewandelt werden. Sollte der übergebene
Zeiger pHeightMap ins Leere gehen wird 0 zurückgeliefert, ansonsten die
gewünschte Höhe.
int Height(BYTE *pHeightMap, int X, int Y)
// This Returns The Height From A Height Map Index
{
int x = X % MAP_SIZE; // Error Check Our x Value
int y = Y % MAP_SIZE; // Error Check Our y Value
if(!pHeightMap) return 0; // Make Sure Our Data Is Valid
Das eigentlich eindimensionale Array pHeightMap[x] wird durch die Gleichung
index = (x + (y * MAP_SIZE) ) wie ein zweidimensionales Array pHeightMap[x][y]
behandelt.
return pHeightMap[x + (y * MAP_SIZE)]; // Index Into Our Height Array And Return The Height
}
Hier wird die Farbe eines anzuzeigenden Punktes aus seiner Höhe generiert.
Um das Ganze ein wenig dunkler aussehen zu lassen, wird 0.15f von dem eigentlichen
Wert subtrahiert. Je höher der Punkt liegt, destso heller ist er, die Höhe
wird durch 256 geteilt um Werte zwischen 0.0f und 1.0f zu erzeugen. Letztendlich
liegt der Farbwert also zwischen -0.15f und 0.85f. Mit glColor3f wird die Farbe
an OpenGL übermittelt, fColor wird hier als Blauwert übergeben, die
anderen Farbkomponenten können aber natürlich auch angepasst werden.
void SetVertexColor(BYTE *pHeightMap, int x, int y)// This Sets The Color Value For A Particular Index
{
// Depending On The Height Index
if(!pHeightMap) return;// Make Sure Our Height Data Is Valid
float fColor = -0.15f + (Height(pHeightMap, x, y ) / 256.0f);
// Assign This Blue Shade To The Current Vertex
glColor3f(0.0f, 0.0f, fColor );
}
Die Heightmap wird in der nächsten Funktion gerendert:
void RenderHeightMap(BYTE pHeightMap[])// This Renders The Height Map As Quads
{
int X = 0, Y = 0; // Create Some Variables To Walk The Array With.
int x, y, z; // Create Some Variables For Readability
if(!pHeightMap) return; // Make Sure Our Height Data Is Valid
bRender bestimmt, ob die Landschaft als Gitternetz (GL_LINES) oder als feste
Oberfläche (GL_QUADS) gerendert werden soll.
if(bRender) // What We Want To Render
glBegin( GL_QUADS ); // Render Polygons
else
glBegin( GL_LINES ); // Render Lines Instead
Im unteren Abschnitt wird die Landschaft aus der Heightmap generiert. Mit Hilfe
der zwei FOR-Schleifen wird das Array der Heightmap im Ganzen durchgegangen
und die zu zeichnenden Punkte zu Quadern oder Linien zusammengesetzt. X und
Y sind die jeweiligen Koordinaten im Array und gleichzeitig die Koordinaten
in der Landschaft. Hier läßt sich auch nachvollziehen, warum ein
zu kleiner Wert für STEP_SIZE die Framerate deutlich beeinträchtigen
kann, die beiden Schleifen werden dann nämlich sehr oft durchlaufen und
brauchen entsprechend viel Zeit. Zu hohe STEP_SIZE-Werte würden das Gelände
aber auch sehr kantig erscheinen lassen. 16 ist ein recht guter Mittelwert,
schnellere PCs/Grafikkarten (2003) werden aber sicherlich auch noch mit 8 sehr
gut zurecht kommen. Eine andere Form der Beleuchtung und Texturen können
allerdings auch sehr kantige Heightmaps verbessern.
for ( X = 0; X < (MAP_SIZE-STEP_SIZE); X += STEP_SIZE )
for ( Y = 0; Y < (MAP_SIZE-STEP_SIZE); Y += STEP_SIZE )
{
// Get The (X, Y, Z) Value For The Bottom Left Vertex
x = X;
y = Height(pHeightMap, X, Y );
z = Y;
// Set The Color Value Of The Current Vertex
SetVertexColor(pHeightMap, x, z);
glVertex3i(x, y, z);// Send This Vertex To OpenGL To Be Rendered
// Get The (X, Y, Z) Value For The Top Left Vertex
x = X;
y = Height(pHeightMap, X, Y + STEP_SIZE );
z = Y + STEP_SIZE ;
// Set The Color Value Of The Current Vertex
SetVertexColor(pHeightMap, x, z);
glVertex3i(x, y, z);// Send This Vertex To OpenGL To Be Rendered
// Get The (X, Y, Z) Value For The Top Right Vertex
x = X + STEP_SIZE;
y = Height(pHeightMap, X + STEP_SIZE, Y + STEP_SIZE );
z = Y + STEP_SIZE ;
// Set The Color Value Of The Current Vertex
SetVertexColor(pHeightMap, x, z);
glVertex3i(x, y, z);// Send This Vertex To OpenGL To Be Rendered
// Get The (X, Y, Z) Value For The Bottom Right Vertex
x = X + STEP_SIZE;
y = Height(pHeightMap, X + STEP_SIZE, Y );
z = Y;
// Set The Color Value Of The Current Vertex
SetVertexColor(pHeightMap, x, z);
glVertex3i(x, y, z);// Send This Vertex To OpenGL To Be Rendered
}
glEnd();
Zum Schluß wird die Farbe auf Weiss
gesetzt, damit dananch gezeichnete Objekte nicht blau gefärbt werden.
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);// Reset The Color
}
In DrawGLScene wird die Funktion gluLookAt() aufgerufen mit 9 Parametern, also
der Position des Betrachters, dessen Blickrichtung und ein nach oben zeigender
Vektor. Dies legt sehr genau fest in welchem Winkel und von wo aus der Betrachter
auf die Szene schaut. In diesem Beispiel befindet sich der Betrachter im Punkt
(212, 60, 194) und schaut in Richtung des Punktes (186, 55, 171) der sich zwischen
ihm und der Szene befindet. Die letzten drei Parameter gehören zu dem Vektor,
der festlegt, wo beim Betrachter "oben" sein soll. Dem normalen Seheindruck
(auch am Monitor) entspricht (0,1,0) sicherlich am meisten, da wir einen Punkt
in einem gedachten Koordinatensystem mit den Koordinaten (0,1,0) auch als "über"
uns bezeichnen würden, wenn wir dabei im Koordinatenursprung ständen
und nach vorne schauten.
Sollte die Erklärung zu schlimm gewesen sein, lohnt es sich eigene Werte
auszuprobieren.
int DrawGLScene(GLvoid)// Here's Where We Do All The Drawing
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Clear The Screen And The Depth Buffer
glLoadIdentity();// Reset The Matrix
// Position, View, Up Vector
gluLookAt(212, 60, 194, 186, 55, 171, 0, 1, 0);
// This Determines The Camera's Position And View
Durch glScalef() wird die Szene auf die die vom Benutzer eingestellte Größe
(Hoch-/Runtertaste) skaliert.
glScalef(scaleValue, scaleValue * HEIGHT_RATIO, scaleValue);
Wenn die Szene gedreht oder verschoben werden soll, kann das entweder mit den
üblichen OpenGL-Funktionen passieren (glTranslatef(), glRotate(), usw)
oder durch neue, an die RenderHeightMap-Funktion übergebene Parameter.
RenderHeightMap(g_HeightMap); // Render The Height Map
return TRUE; // Keep Going
}
KillGLWindow() und CreateGLWindow() bleiben so wie sie sind, nur in WndProc()
soll überprüft werden, ob die linke Maustaste gedrückt wurde.
Damit soll zwischen Gitternetz und festem Untergrund hin und her geschaltet
werden könen.
...
case WM_LBUTTONDOWN: // Did We Receive A Left Mouse Click?
{
bRender = !bRender; // Change Rendering State Between Fill/Wire Frame
return 0; // Jump Back
}
...
In WinMain muss auch kaum was verändert werden, nur der Programmtitel
und einige Abfragen zu den benutzten Tasten.
...
if (!CreateGLWindow("NeHe & Ben Humphrey's Height Map Tutorial",
640, 480, 16, fullscreen))
...
Der untere Code (ganz am Ende von WinMain) erhöht oder verringert den
Skalierungswert für die Szene, je nachdem, ob die Hoch- oder Runtertaste
betätigt wurde.
...
if (keys[VK_UP])// Is The UP ARROW Being Pressed?
scaleValue += 0.001f;// Increase The Scale Value To Zoom In
if (keys[VK_DOWN])// Is The DOWN ARROW Being Pressed?
scaleValue -= 0.001f;// Decrease The Scale Value To Zoom Out
}
}
// Shutdown
KillGLWindow();// Kill The Window
return (msg.wParam);// Exit The Program
}
Das wars auch schon! Ich hoffe das Tut war interessant und regt neue Ideen
an, es könnte z.B. Texturen, Beleuchtung oder weitere Objekte eingebunden
werden, an sehr tiefen Stellen blaue Texturen für Wasser, darüber
grünliche für die Landschaft, später bräunliche an Bergen
und weiße Gletscher oben drauf, irgendwas in der Art.
Auf http://www.GameTutorials.com.
gibt es noch weitere Tuts von Ben Humphrey.
Ben Humphrey (DigiBen)
Jeff Molofee (NeHe) http://nehe.gamedev.net
Die Source Codes und Ausführbaren Dateien zu den Kursen liegen auf der Neon
Helium Website
codeworx.
Übersetzt und modifiziert von Hans-Jakob Schwer 05.10.2k3, www.codeworx.org