www.codeworx.org/opengl-tutorials/Tutorial 34: Landschaften aus Heightmaps generieren

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