www.codeworx.org/c++ tuts /Kongos rand() Tutorials, 6: Terrain-Rendering Teil 1

Tutorial 6: Terrain-Rendering Teil 1

(Stand: 2002-02-28 Fertig zu 5%)

Willkommen zum sechsten und wohl ausführlichsten Tutorial, das ich jemals schreibe und schreiben werde. Ja, werde. Das Tutorial wird sich ca. alle 2 Wochen mit einem neuen Thema füllen. Begonnen wird einfach damit, ein Terrain zu rendern ohne irgendwelche Gimmicks. Später soll diese Welt (die da ja entsteht) unendlich groß werden, so dass man an keine unsichtbaren Wände stößt und nicht mehr weiter kommt. Zusätzlich sollen noch Dinge wie Sonne, Nebel, Himmel, Bäume (ja ich meine echte 3D Bäume *g*),.. implementiert werden, aber dazu später. Viel wichtiger ist es jedoch, eine Art von Level of Detail System zu programmieren. Beginnen wir aber mit der einfachen Darstellung eines Terrain.

Download der Beispielprogramme

Basic.Zip Um jedes Beispielprogramm zum laufen zu kriegen oder es zu kompilieren, muss man das Basic-Zip downloaden. Dies beinhaltet die Zed 4D Engine + eine Heightmap (dazu gleich mehr). Diese Package sollte man in jedes Beispielprogramm entzippen. Dadurch ist das Projekt kompilierbar und lauffähig.
Brute-Force Attack Version1
Brute-Force Attack Version2

Brute-Force Attack Versuch 1

Unser erster Ansatz ist wirklich ganz einfach. Das Terrain hat eine bestimmte Größe von 128*128 (m,cm, was auch immer) und besitzt keine Textur. Es wird nur eine Farbe gezeigt, die anhand der Höhe des Terrain berechnet wird. Weiters kann man einfach in der 3D Welt herumfliegen ohne irgendwelche Kollisionserkennung.

Beginnen wir einfach damit uns die Klasse zTerrain anzuschauen, die alle Terrain-wichtigen Arbeiten verrichtet.

struct terrvertex 
{
   float x,y,z;
   DWORD diffuse;
};
#define D3DFVF_TERRAIN (D3DFVF_XYZ|D3DFVF_DIFFUSE) 
   

So sieht ein Terrain-Vertex aus. Zur Zeit benötigen wir nur die Position und eine Farbe. Später werden hier noch Textur-Werte,... zu finden sein.

#define ind(x, z) ((x) + ((z) * 128)) 

Diese Makro verwenden wir um von einer 2D Position in ein 1D Array zu zeigen.

class zTerrain
{
private:
   terrvertex *aterr;
   short *aheightmap;
public:
   zTerrain();
   ~zTerrain();
   bool Create(const char* name);
   void Destroy();
   void Draw(zVector3 pos);
}; 


Wir benötigen insgesamt nur 3 Funktionen um unser erstes Terrain darzustellen. Der Funktion Create() übergeben wir einen Dateinamen. Diese Datei habe ich in Adobe Photoshop gemacht. Es ist eine 128*128 Pixel großes Bild im Modus Graustufe. Graustufe bedeutet soviel wie 8-bit. Das heißt wir bekommen eine 128*128*8 bit große Datei (oder 128*128 byte große Datei). Meine sieht so aus:

Wir verwenden diese Grafik um Höhenwerte für unser Terrain zu erstellen. Jeder Pixel ist dabei ein Feld in unserem Terrain. Weiß bedeutet da so viel wie 255, also hohe Position. Schwarz ist eine 0, also sehr tief. Weiter im Text...

Beginnen wir nun mit der Funktion Create().

bool zTerrain::Create(const char* name)
{
   ifstream file;
   unsigned char c;
   int num=0;
   aheightmap = (short*) malloc(128*128*sizeof(short));
   aterr = (terrvertex*) malloc(127*127*6*sizeof(terrvertex));
   .... 


Zuerst allokieren wir Speicher. Zuerst für die Heightmap und dann für unser Terrainfeld. Wie du siehst allokieren wir für die Heightmap 128*128 Shorts, wogegen wir nur 127*127 für das Terrain anlegen und dieses dann sogar noch mit *6 multiplizieren. Das *6 liegt daran, das jeder Heightmap-Punkt (also jeder Terrainhöhenpunkt) durch 6 Vertices dargestellt wird. Warum wir aber nur 127*127 rechnen und nicht 128*128, dazu gleich mehr.

   ....
   file.open(name,ios::binary);
   for (int z=0; z < 128; z++) {
      for (int x=0; x < 128; x++) {
         file.get(c);
         aheightmap[ind(x,z)] = (short)c;
      }
   }
   file.close();
   .... 


Jetzt öffnen wir die Datei und lesen die Werte in das Heightmap-Array. Das Array besitzt jetzt lauter Werte von 0 - 255.

   ....
   for (z=0; z < 127; z++) {
   for (int x=0; x < 127; x++) {
   aterr[num].x = (float) x;
   aterr[num].z = (float) z;
   aterr[num].y = (float)aheightmap[ind(x,z)] / 25.6f;
   aterr[num].diffuse = aheightmap[ind(x,z)];
   aterr[num+1].x = (float) x+1;
   aterr[num+1].z = (float) z;
   aterr[num+1].y = (float)aheightmap[ind(x+1,z)] / 25.6f;
   aterr[num+1].diffuse = aheightmap[ind(x,z)];
   aterr[num+2].x = (float) x+1;
   aterr[num+2].z = (float) z+1;
   aterr[num+2].y = (float)aheightmap[ind(x+1,z+1)] / 25.6f;
   aterr[num+2].diffuse = aheightmap[ind(x,z)];
   aterr[num+3].x = (float) x;
   aterr[num+3].z = (float) z;
   aterr[num+3].y = (float)aheightmap[ind(x,z)] / 25.6f;
   aterr[num+3].diffuse = aheightmap[ind(x,z)];
   aterr[num+4].x = (float) x+1;
   aterr[num+4].z = (float) z+1;
   aterr[num+4].y = (float)aheightmap[ind(x+1,z+1)] / 25.6f;
   aterr[num+4].diffuse = aheightmap[ind(x,z)];
   aterr[num+5].x = (float) x;
   aterr[num+5].z = (float) z+1;
   aterr[num+5].y = (float)aheightmap[ind(x,z+1)] / 25.6f;
   aterr[num+5].diffuse = aheightmap[ind(x,z)];
   num += 6;
   }
}
return true;
} 

Puh, diese Schleife ist ein bisschen lang, aber sie ist nicht schwer zu verstehen. Wie schon gesagt, legen wir jetzt für jeden Punkt 6 Vertices an, also 2 Dreiecke. Diese Dreiecke bilden ein Quadrat. Die Höhe bestimmt die Heightmap. Wir dividieren den Heightmapwert aber durch 25.5, um so Höhenwerte von 0 - 10 zu bekommen. Die Farbe bestimmt auch die Heightmap. Der Heightmap-Wert ist einfach die Farbe (alle möglichen Blautöne).

Warum ist unser Terrain jetzt aber nur 127*127 groß? Ganz einfach. Wenn wir den Punkt x=127, z=127 anlegen, berechnet dieser seine Werte ja auch aus den Werte um ihn herum. Also auch aus dem Punkt x=128,z=128. Dadurch wird diese Punkt sowieso dargestellt.

Das war eigentlich die Größte der drei Funktionen. Zur Vervollständigung hier noch die zwei anderen.

void zTerrain::Destroy()
{
   free(aterr);
   free(aheightmap);
}
void zTerrain::Draw(zVector3 pos)
{
   _setvs(D3DFVF_TERRAIN);
   _drawpu(D3DPT_TRIANGLELIST,127*127*2,aterr,sizeof(terrvertex));
} 


In der Funktion Draw() verwende ich einfach Makros, wie z.B. _setvs um wir das Schreiben von pDev->SetVertexShader() zu ersparen. (Fauler Sack!!!) _drawpu ist dabei ein Makro für pDev->DrawPrimitiveUP().

Brute-Force Attack Versuch 2

Die Verbesserung diese Programms ist ganz einfach, dass ein Vertex Buffer verwendet wird, um das Terrain zu speichern. Wenn sich jetzt einer fragt: Ja warum hat er das nicht schon im ersten Programm getan? Möchte ich folgendes dazu sagen: Ich wollte einfach mal zeigen, um wie viel schneller Vertex Buffer im Gegensatz zu einem einfache Array sind. Auf meinem Computer bekam ich für das erste Programm eine FPS von rund 117 und für das zweite Programm eine FPS von rund 190.

Fortsetzung folgt....

Copyright (c) by Kongo

Bei Fragen, Beschwerden, Wünschen schreib an kongo@codeworx.org