[Tutorial] Terrain in DirectX 9 und C++ - Teil 1

Anfänger92

Erfahrenes Mitglied
Hallo,

also ich habe mal nachgeschaut und noch kein Tutorial in die Richtung gefunden.

Da ich mich vor kurzem selbst ein wenig mit dem Thema befasst habe möchte ich anderen das Thema jetzt leichter machen.

Nur eine Frage bevor ich poste:

Kann ich das Tutorial auch auf meine Seite stellen wenn ich es hier reinstelle oder nicht?

Gruß,
Anfänger
 
Grundsätzlich erheben wir kein Recht auf Exklusivität ABER
wir freuen uns, wenn es Jemand aus freien Stücken macht.

mfg chmee
 
Nun gut ich lass mir das nochmal durch den Kopf gehen. Aber hier erstmal der erste Teil zur Kontrolle :p.

Edit:
Der größte Teil der Formatierung wird leider nicht übernommen. Kann man da i-wie was ändern ?

Nochmal Edit:
Hab gerade gesehen manche Tutorials sind auch als PDF. Ist villeicht besser damit die Formatierungen bleiben.
 
Zuletzt bearbeitet:
Terrain in DirectX 9 und C++ - Teil 1

Das hier ist mein erstes Tutorial und soll zeigen, wie man ein beliebig großes Terrain aus einer Height-Map generiert und darstellt.

Ich werde hier oft das Wort Terrain benutzten, damit meine ich eine Landschaft erstellt aus einer Height-Map.
Außerdem werde ich das Wort Height-Map oft benutzen, deshalb schreibe ich einfach HMAP.

Wichtig: Ich werde nicht auf Algorithmen zur Reduktion der Detailstufe eingehen, wir werden lediglich simple Techniken verwenden, die uns aber trotzdem ermöglichen ein Terrain brauchbarer Größe darzustellen.

1. Die HMAP
2. Grundwissen zu Terrains
3. Das Grundgerüst unserer Anwendung.
4. Die HMAP laden und darstellen.
5. Anhang

1. Die HMAP

Die HMAP ist eigentlich nur ein Bild einer bestimmten Größe, welches die Höhenwerte eines Punktes im Terrain speichert.

Die Höhenwerte werden in Graustufen dargestellt, wobei Weiß ganz hoch und Schwarz ganz niedrig ist.
Also gilt: Je heller desto höher.

Eine HMAP erstellt man in einem Bild-Bearbeitungsprogramm (Ja auch Paint tut es schon).

Ist die HMAP erstellt speichert man sie, wobei ich es bevorzuge sie als .raw abzuspeichern, bei einem Kanal (1 Byte pro Pixel).
Worum wird sich da so mancher fragen! : Das hat einen ganz einfachen Grund. Speichert man das Ganze als .bmp oder ähnliches ab, muss man das Bild erst von einer Funktion laden lassen, z.B. D3DXCreateTexture..., und kann dann auf die Höhenwerte zugreifen. Bei einer .raw Datei haben wir es hier einfacher. Die Bytes der Pixel liegen einfach hintereinander in der Datei und wir müssen sie nur in den Speicher laden.

Falls ihr kein Programm habt welches euer Bild als .raw abspeichern kann, versucht es mit Gimp oder benutzt das kleine Tool das sich im Anhang befindet, und aus einer .bmp Datei eine .raw Datei mit einem Byte pro Pixel erstellt.

Naja nun sollten wir alle eine HMAP erstellen und speichern. Achtet dabei darauf das unsere HMAP für unseren ersten Versuch 64x64 Pixel groß sein soll.
Meine sieht so aus (Download im Anhang):


[BILD]

Wichtig: Beim erstellen der HMAP in Gimp oder PS solltet ihr darauf achten, dass eure Pinsel weich genug eingestellt sind, damit es im Terrain später nicht so harte Kanten gibt.

Außerdem solltet ihr darauf achten das wirklich nur die Pixel und auch nur 1 Byte pro Pixel gespeichert werden. Man kann es ganz leicht kontrollieren indem man auf die Größe der Datei schaut. Sie muss genau GrößeX mal GrößeY Bytes groß sein (Bsp.: 64x64 = 4096).

2. Grundwissen zu Terrains

Da man als Entwickler von Spielen natürlich möchte, dass sein Terrain gut aussieht und auch weit sichtbar ist muss man auf verschiedene Techniken zurückgreifen.
Ohne solche Techniken ist ein Terrain wie das, welches wir am Ende dieser Tutorialreihe erhalten, unmöglich darzustellen.

Am Ende werden wir ein Terrain auf den Bildschirm zaubern, das 3072x3072 Einheiten groß ist.
Das bedeutet: Mit der BruteForce-Methode würden wir 18.874.368 Dreiecke an die Grafikkarte schicken. Pro Bild!
Diese ungeheure Menge kann eine Grafikkarte natürlich kaum bewältigen, und wenn, fehlen auch noch die restlichen Modelle in unserer Welt.

Also müssen ein paar andere Techniken her.
In diesem Tutorial werden wir lediglich ein paar Sichtbarkeits-Test durchführen, und einen kleinen Trick verwenden, um die Detailstufe in größerer Entfernung noch zu verringern.
Diese zwei Techniken werden uns aber einen großen Geschwindigkeits-Vorteil bringen.

Natürlich gibt es viele Techniken um die Anzahl der Dreiecke eins Terrains auch im Allgemeinen zu verringern, z.B. den ROAM-Algorithmus.

3. Das Grundgerüst unserer Anwendung

Zu aller erst gehe ich einmal davon aus, dass eurem Compiler usw. die Pfade zu den DirectX Verzeichnissen bekannt sind.
Ich benutzte hier das MS Visual Studio.

Also wir starten ein neues Win32 Projekt und fügen eine .cpp Datei hinzu, ich nenne sie "main.cpp"

Nun erstellen wir ein Grundgerüst. Wir erstellen ein Fenster und ein Direct3DDevice dazu. Da ich davon ausgehe, jeder der sich um diesem Thema kümmert, weiß wie so etwas geht, werde ich den Code nicht mehr erklären, nur noch abdrucken. Ihr findet das Projekt in diesem Zustand im Anhang als "Step_01.zip"

#include <Windows.h>
#include <D3D9.h>
#include <D3DX9.h>

#pragma comment(lib, "D3D9.lib")
#pragma comment(lib, "D3DX9.lib")

PDIRECT3D9 g_pD3D = NULL;
PDIRECT3DDEVICE9 g_pD3DDevice = NULL;
HWND g_hMainWindow = NULL;

LRESULT CALLBACK WndProc(HWND hWnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
switch(uiMsg)
{
default:
return DefWindowProc(hWnd,uiMsg,wParam,lParam);
}
return 0;
}

int WINAPI WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nShowCmd)
{
//Create a Window
WNDCLASS WndClass;
ZeroMemory(&WndClass,sizeof(WNDCLASS));
WndClass.hbrBackground = (HBRUSH)BLACK_BRUSH;
WndClass.hInstance = hInstance;
WndClass.lpfnWndProc = (WNDPROC)WndProc;
WndClass.lpszClassName = L"MyWndClass";
WndClass.style = CS_VREDRAW|CS_HREDRAW;
RegisterClass(&WndClass);
g_hMainWindow = CreateWindowW( L"MyWndClass",
L"Terrain Tutor using DirectX and C++",
WS_VISIBLE,
CW_USEDEFAULT,CW_USEDEFAULT,
800,600,
NULL,NULL,
hInstance,NULL);
if(!g_hMainWindow) //Error -> Exit
return FALSE;
//Create the Direct3D-Interface
g_pD3D = Direct3DCreate9(D3D_SDK_VERSION);
if(!g_pD3D) //Error -> Exit
return FALSE;
//Fill the Present Parameters
D3DPRESENT_PARAMETERS PresentParams;
ZeroMemory(&PresentParams,sizeof(D3DPRESENT_PARAMETERS));
PresentParams.AutoDepthStencilFormat = D3DFMT_D24S8;
PresentParams.BackBufferCount = 1;
PresentParams.BackBufferFormat = D3DFMT_X8R8G8B8;
PresentParams.BackBufferHeight = 600;
PresentParams.BackBufferWidth = 800;
PresentParams.EnableAutoDepthStencil = TRUE;
PresentParams.Flags = 0;
PresentParams.hDeviceWindow = g_hMainWindow;
PresentParams.PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT;
PresentParams.SwapEffect = D3DSWAPEFFECT_DISCARD;
PresentParams.Windowed = TRUE;
//Create the Direct3DDevice-Interface
if(FAILED(g_pD3D->CreateDevice( 0,
D3DDEVTYPE_HAL,
g_hMainWindow,
D3DCREATE_MIXED_VERTEXPROCESSING,
&PresentParams,
&g_pD3DDevice)))
{
return FALSE;
}

MSG msg;
while(1)
{
while(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
//Here we will render the game later
}
return TRUE;
};


Führt man diesem Code aus sollte man folgendes Ergebnis erhalten:

[BILD]
4. Die HMAP laden und darstellen.

Als nächstes müssen wir die HMAP laden.

Dazu benötigen wir als erstens einmal ein paar Datei-Funktionen. Ich werde hier fopen, fread und fclose benutzten, also fügen wir noch die Datei "stdio.h" hinzu.
#include <Windows.h>
#include <stdio.h>
...

Nun schreiben wir eine Funktion die unsere Daten einliest:
BOOL LoadHMAP(char* pcHMAP, UINT SizePerSide, BYTE **ppData)
{
BYTE *pData = NULL;
FILE *pFile = NULL;
//Open file as binary for reading
pFile = fopen(pcHMAP,"rb");
if(!pFile)
return FALSE;
//allocate memory
pData = new BYTE[SizePerSide*SizePerSide];
//read data
fread(pData,1,SizePerSide*SizePerSide,pFile);
//finish
*ppData = pData;
return TRUE;
}
Als erstes öffnen wir die Datei.
Dann fordern wir Speicher an, und zwar genau 1 Byte pro Pixel.
Anschließend lesen wir die Daten ein und das war es auch schon.


Um das zuweisen einer Position später einfacher zu machen definiere ich jetzt erst einmal eine simple 3D-Vektor Struktur
//simple 3D-vector structure
struct
Vector3
{
public:
float x, y, z;
Vector3(float fx, float fy, float fz)
{
x = fx;
y = fy;
z = fz;
};
Vector3()
{
x = y = z = 0;
};
};

So, als nächstes benötigen wir ein eigens Vertex-Format.
Auch wenn uns derzeit ein einfacher Vektor genügen würde, erstellen wir dafür eine Struktur, um es später einfacher erweitern zu können.

Unseren Vektor definiere ich wie folgt:
//Our terrain-vector
struct
STerrainVektor
{
Vector3 vPos;
};

Nun müssen wir aus den eingelesenen Daten noch unsere Vertex-Daten erstellen.
Dafür schreiben wir auch eine eigene Funktion.
Sie soll einfach Speicher für die Vertieces anfordern und die dann erstellen.
Wir benötigen genau Speicher für 63x63x2 Dreiecke, bei 3 Vertieces pro Dreieck.

Einige werden sich wahrscheinlihc fragen, warum es nur 63 Dreiecke pro Seite sind, wo unsere HMAP doch 64 Pixel pro Seite hat.
Das hat einen ganz einfach Grund:
Z.B. wird für das erste Quadraht in einer Reihe jeweils der erste und zweite Pixel auf der X-Achse benötigt,
für das zweite Quadraht der der zwiete und dritte Pixel usw.
Ist man beim 64 Quadraht in einer Reihe angekommen, wird man merken das es aber einen 65 Pixel benötigt.
Da wir diesen nicht haben müssen wir uns mit 63 Quadrahten pro Seite zufrieden geben.

Also hier ist die Funktion:
BOOL CreateVertieces( UINT SizePerSide, BYTE *pVertexData, STerrainVektor **ppVertieces,UINT *uiTriangleCount)
{
UINT uiSizePerSide = SizePerSide - 1;
*uiTriangleCount = uiSizePerSide*uiSizePerSide*2;
//We need mem for SizeX * SizeY * (2 triangle per square) * (3 vertieces per triangle)
STerrainVektor *pVertieces = new STerrainVektor[(*uiTriangleCount)*3];
//Fill Buffer
int Index = 0;
for(int x = 0; x < uiSizePerSide; x++)
for(int z = 0; z < uiSizePerSide; z++)
{
Index+=6;
pVertieces[Index+0].vPos = Vector3(x,pVertexData[z*SizePerSide+x],z);
pVertieces[Index+1].vPos = Vector3(x,pVertexData[(z+1)*SizePerSide+x],z+1);
pVertieces[Index+2].vPos = Vector3(x+1,pVertexData[z*SizePerSide+x+1],z);
pVertieces[Index+3].vPos = Vector3(x+1,pVertexData[(z)*SizePerSide+x+1],z);
pVertieces[Index+4].vPos = Vector3(x,pVertexData[(z+1)*SizePerSide+x],z+1);
pVertieces[Index+5].vPos = Vector3(x+1,pVertexData[(z+1)*SizePerSide+x+1],z+1);
}
//ok
*ppVertieces = pVertieces;
return TRUE;
}
Also ich denke auch diese Funktion ist soweit klar, aber hier trotzdem noch ein paar Erklärungen:
Als erstes Berechnen wir die Anzahl der Quadrahte pro Seite und errechnen die Anzahl der Dreiecke.
Anschließend fordern wir Speicher für die Dreiecke an. Genau 3 Vertieces pro Dreieck.
Als letztes werden die Quadrahte erstellt.

Wichtig: Beachtet wo ich die Anzahl der Pixel benutzte und wo cih die Anzahl der Quadrate benutzte. Das ist wichtig damit das Terrain korrekt erstellt wird.
So das war es auch schon fast: Wir müssen nurnoch unsere Nachrichtenschleife anpassen und die Daten laden.
Also als erstes lassen wir die HMAP laden und die Vertieces erstellen. Das geschieht aber noch vor der Nachrichtenschleife:
//Load Data
BYTE *pHeightData = NULL;
STerrainVektor *pVertieces = NULL;
UINT uiTriangleCount = 0;

if(!LoadHMAP("MyFirstHMAP.raw",64,&pHeightData))
return FALSE;
if(!CreateVertieces(64,pHeightData,&pVertieces,&uiTriangleCount))
return FALSE;
Direkt im Anschluss daran erstellen wir eine View- und eine Projektion-Matrix und setzten diese.
Außerdem deaktivieren wir die Beleuchtung, da wir sind nur ein schwarzes Gebilde erhalten.

#define FIELDOFVIEW ( (70.0f/90.0f)*(D3DX_PI/2.0f) )
//Setup transformations
D3DXMATRIX mProj;
D3DXMATRIX mView;
D3DXMatrixPerspectiveFovLH(&mProj,FIELDOFVIEW,800/600,0.1f,9000.0f);
D3DXMatrixLookAtLH(&mView,&D3DXVECTOR3(-10,130,-10),&D3DXVECTOR3(0.0f,100.0f,0.0f),&D3DXVECTOR3(0.0f,1.0f,0.0f));
g_pD3DDevice->SetTransform(D3DTS_PROJECTION,&mProj);
g_pD3DDevice->SetTransform(D3DTS_VIEW,&mView);

g_pD3DDevice->SetRenderState(D3DRS_LIGHTING,FALSE);

So nun müssen wir nurnoch unser Terrain anzeigen lassen:
//Here we will render the game later
g_pD3DDevice->Clear(0,NULL,D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,0x0044ff,1.0f,0.0);
g_pD3DDevice->BeginScene();
g_pD3DDevice->SetFVF(D3DFVF_XYZ);
g_pD3DDevice->DrawPrimitiveUP(D3DPT_TRIANGLELIST,uiTriangleCount,(void*)pVertieces,sizeof(STerrainVektor));
g_pD3DDevice->EndScene();
g_pD3DDevice->Present(NULL,NULL,NULL,NULL);

Nun kopieren wir nurnoch unsere HMAP mit dem Namen „MyFirstHMAP.raw“ in unser Projektverzeichnis und startet das Programm.
So ersteinmal geschafft. Das Terrain wird geladen und angezeigt. Nachdem wir jetzt auch unsere Umgebung soweit eingerichtet haben können wir uns auch mehr auf das eigentliche Thema, das Terrain, konzentrieren.
Das Projekt bis zu diesem Schritt ist im Anhang unter „Step_02.zip“ zu finden. Hier nochmal die vollständige WinMain-Funktion:
int WINAPI WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nShowCmd)
{
//Create a Window
WNDCLASS WndClass;
ZeroMemory(&WndClass,sizeof(WNDCLASS));
WndClass.hbrBackground = (HBRUSH)BLACK_BRUSH;
WndClass.hInstance = hInstance;
WndClass.lpfnWndProc = (WNDPROC)WndProc;
WndClass.lpszClassName = L"MyWndClass";
WndClass.style = CS_VREDRAW|CS_HREDRAW;
RegisterClass(&WndClass);
g_hMainWindow = CreateWindowW( L"MyWndClass",
L"Terrain Tutor using DirectX and C++",
WS_VISIBLE,
CW_USEDEFAULT,CW_USEDEFAULT,
800,600,
NULL,NULL,
hInstance,NULL);
if(!g_hMainWindow) //Error -> Exit
return FALSE;
//Create the Direct3D-Interface
g_pD3D = Direct3DCreate9(D3D_SDK_VERSION);
if(!g_pD3D) //Error -> Exit
return FALSE;
//Fill the Present Parameters
D3DPRESENT_PARAMETERS PresentParams;
ZeroMemory(&PresentParams,sizeof(D3DPRESENT_PARAMETERS));
PresentParams.AutoDepthStencilFormat = D3DFMT_D24S8;
PresentParams.BackBufferCount = 1;
PresentParams.BackBufferFormat = D3DFMT_X8R8G8B8;
PresentParams.BackBufferHeight = 600;
PresentParams.BackBufferWidth = 800;
PresentParams.EnableAutoDepthStencil = TRUE;
PresentParams.Flags = 0;
PresentParams.hDeviceWindow = g_hMainWindow;
PresentParams.PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT;
PresentParams.SwapEffect = D3DSWAPEFFECT_DISCARD;
PresentParams.Windowed = TRUE;
//Create the Direct3DDevice-Interface
if(FAILED(g_pD3D->CreateDevice( 0,
D3DDEVTYPE_HAL,
g_hMainWindow,
D3DCREATE_MIXED_VERTEXPROCESSING,
&PresentParams,
&g_pD3DDevice)))
{
return FALSE;
}

//Load Data
BYTE *pHeightData = NULL;
STerrainVektor *pVertieces = NULL;
UINT uiTriangleCount = 0;

if(!LoadHMAP("MyFirstHMAP.raw",64,&pHeightData))
return FALSE;
if(!CreateVertieces(64,pHeightData,&pVertieces,&uiTriangleCount))
return FALSE;

#define FIELDOFVIEW ( (70.0f/90.0f)*(D3DX_PI/2.0f) )
//Setup transformations
D3DXMATRIX mProj;
D3DXMATRIX mView;
D3DXMatrixPerspectiveFovLH(&mProj,FIELDOFVIEW,800/600,0.1f,9000.0f);
D3DXMatrixLookAtLH(&mView,&D3DXVECTOR3(-10,130,-10),&D3DXVECTOR3(0.0f,100.0f,0.0f),&D3DXVECTOR3(0.0f,1.0f,0.0f));
g_pD3DDevice->SetTransform(D3DTS_PROJECTION,&mProj);
g_pD3DDevice->SetTransform(D3DTS_VIEW,&mView);

g_pD3DDevice->SetRenderState(D3DRS_LIGHTING,FALSE);

MSG msg;
while(1)
{
while(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
//Here we will render the game later
g_pD3DDevice->Clear(0,NULL,D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,0x0044ff,1.0f,0.0);
g_pD3DDevice->BeginScene();
g_pD3DDevice->SetFVF(D3DFVF_XYZ);
g_pD3DDevice->DrawPrimitiveUP(D3DPT_TRIANGLELIST,uiTriangleCount,(void*)pVertieces,sizeof(STerrainVektor));
g_pD3DDevice->EndScene();
g_pD3DDevice->Present(NULL,NULL,NULL,NULL);
}
return TRUE;
};

Das Ergebniss sollte so aussehen:

[BILD]
5. Anhang
Step_01.zip
Step_01.jpg
Step_02.zip
Step_02.jpg
HMAP_64x64.bmp
HMAP_64x64.raw
BmpToRaw.zip
 
Geh damit bitte mal in den Tutorials-Bereich und stelle es dort rein.. Da ist es definitiv besser aufgehoben und macht auch mehr her :) Benutze für Scripts/Code auch den Code-Tag, dann sollte es besser lesbar sein.

Werde das hier morgen früh löschen :) mfg chmee
 
Werde das hier morgen früh löschen :)
Aber bitte erst nachdem es bei den Tutorials eingegangen ist. ;)

Und bevor der Thread gelöscht wird.....
Ich habe Null Ahnung von C++ (und habe auch nicht vor es zu ändern ;)) und wüsste somit auch nicht wie ich das Tutorial (gerecht) bewerten sollte.
Daher jetzt noch schnell hier: alleine für den Umfang hast Du meinen aller grössten Respekt verdient!
Da darf man sicherlich auf die Fortsetzung gespannt sein. ;)
 
Hallo Anfänger92,

wieso erstellst du eine eigene Struktur Vector3, wo du doch auch einfach D3DXVECTOR3 verwenden könntest?

Ansonsten solltest du die Rechtschreibung noch etwas überarbeiten ("Quadraht" sollte "Quadrat" heißen, "Vertieces" sind eigentlich "Vertices", ein paar Buchstabendreher, etc.) Da wäre es eigentlich nicht schlecht, wenn wir eine Art Wiki für Tutorials hätten, dann hätte ich das schnell selbst gemacht :)

Eine etwas genauere Erklärung des Triangulierungsschemas wäre auch nett gewesen, aber vielleicht kommt das auch auf den Bildern rüber (die man ja hier nicht sieht).

Bis auf diese Kritikpunkte ist es aber ein schönes Tutorial geworden :)

Grüße, Matthias
 
Ok danke.

Werde das was du gesagt hast nochmal korigieren und eine bessere Beschreibung der Triangulierung machen.

Auch danke an Dr Dau für das Lob.

Habe das Tutorial jetzt auch in der Tutorial-Sektion. Habe es aber dort als PDF hochgeladen.

Gruß Anfänger
 
Zurück