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