Heightfield terrain

In deze tutorial laat ik je zien hoe je makkelijk een terrein kunt maken door gebruik te maken van een bitmap. Het idee hierachter is om de grijs waarden van een bitmap om te zetten in hoogte waarden.

Om te beginnen, de defines, includes, en variabelen.


#define BITMAP_ID 0x4d42
#define MAP_X     32
#define MAP_Z     32
#define MAP_SCALE 40.0f

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <gl/gl.h>
#include <gl/glu.h>

HDC g_HDC;
bool key[256];
bool fullscreen = false;

BITMAPINFOHEADER bmpinfo;
BITMAPINFOHEADER landinfo;

unsigned char* imagedata;
unsigned char* landtexture;
unsigned int land;

float terrain[MAP_X][MAP_Z][3];
Eerst definieren we voor het gemak de bitmap ID om er zeker van te zijn dat er een bitmap geladen gaat worden. We definieren ook de lengte en breedte van de bitmap waarmee we door alle punten van de bitmap kunnen loopen, en de schaal van de map. Includes lijkt me duidelijk en de daarop volgende variabelen. Vervolgens de bitmap variabelen voor de header, de data, en het object. De laatste variabele is een array die de map co÷rdinaten voor stelt.
 
////////////////////////////////////////////////////////////////////////////
//laden van een bitmap
////////////////////////////////////////////////////////////////////////////
unsigned char *loadbmp(char *filename, BITMAPINFOHEADER *bmpinfo)
{
	FILE *fileptr;
	BITMAPFILEHEADER bmpfile;
	unsigned char *bmpimage;
	int imageidx = 0;
	unsigned char swaprgb;

	fileptr = fopen(filename, "rb");
	if (fileptr == NULL){return 0;}

	fread(&bmpfile, sizeof(BITMAPFILEHEADER), 1, fileptr);
	if (bmpfile.bfType != BITMAP_ID)
	{
		fclose(fileptr);
		return 0;
	}

	fread(bmpinfo, sizeof(BITMAPINFOHEADER), 1, fileptr);

	fseek(fileptr, bmpfile.bfOffBits, SEEK_SET);

	bmpimage=(unsigned char*)malloc(bmpinfo->biSizeImage);
	if (!bmpimage)
	{
		free(bmpimage);
		fclose(fileptr);
		return 0;
	}

	fread(bmpimage, 1, bmpinfo->biSizeImage, fileptr);
	if (bmpimage == NULL)
	{
		fclose(fileptr);
		return 0;
	}

	for (imageidx = 0; imageidx < bmpinfo->biSizeImage; imageidx+=3)
	{
		swaprgb = bmpimage[imageidx];
		bmpimage[imageidx] = bmpimage[imageidx + 2];
		bmpimage[imageidx + 2] = swaprgb;
	}

	fclose(fileptr);
	return bmpimage;
}
De functie om een bitmap in te laden zal duidelijk moeten zijn, desnoods kun je altijd nog even de bitmap tutorial doorlezen.

////////////////////////////////////////////////////////////////////////////
//zet de waarden uit de bitmap in een array die de x y en z as voorstelt
////////////////////////////////////////////////////////////////////////////
void InitializeTerrain()
{

	for (int z = 0; z < MAP_Z; z++)
	{
		for (int x = 0; x < MAP_X; x++)
		{
			terrain[x][z][0] = float(x)*MAP_SCALE;				
			terrain[x][z][1] = (float)imagedata[(z*MAP_Z+x)*3];
			terrain[x][z][2] = -float(z)*MAP_SCALE;
		}
	}
}
Zoals je natuurlijk al had verwacht worden hier de co÷rdinaten berekend voor het heightfield. De x en z assen oftewel de lengte en breedte van de map zijn hierbij het makkelijkst, het enigste is de x en z co÷rdinaten vermenigvuldigen met de schaal van de map. De z co÷rdinaten maken we vervolgens negatief zodat deze over de negatieve z as liggen, je kunt dit natuurlijk ook positief laten. De y-as die de hoogte voorsteld is wat moeilijker, een ding wat je hiervoor moet weten is dat de bitmap data in een 1 dimensionele array staat van RGB waardes. Omdat het grijswaarden zijn zijn alle 3 de kleurwaarden gelijk. Nu je dit weet kun je z vermenigvuldigen met map z en daar de x waarde bij optellen. Je weet dat er 3 RGB waarden zijn dus moet je zorgen de uitkomst nog met 3 vermenigvuldigen.

Misschien is het een beetje moeilijk te begrijpen voor de eerste keer maar als je het vaker gebruikt ben je er snel genoeg achter hoe dit precies werkt.


/////////////////////////////////////////////////////////////////////////////
//Rendering
/////////////////////////////////////////////////////////////////////////////
void render()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();

	gluLookAt(1300.0f, 300.0f, -1200.0f, 0.0f, 0.0f, 0.0f, 0.0, 1.0, 0.0);

	glBindTexture(GL_TEXTURE_2D, land);
	for (int z = 0; z < MAP_Z-1; z++)
	{
		glBegin(GL_TRIANGLE_STRIP);
		for (int x = 0; x < MAP_X-1; x++)
		{
		
		  glColor3f(terrain[x][z][1]/255.0f, terrain[x][z][1]/255.0f, 
		  terrain[x][z][1]/255.0f);
		  glTexCoord2f(0.0f, 0.0f);
	          glVertex3f(terrain[x][z][0], terrain[x][z][1], 
		  terrain[x][z][2]);

		  glTexCoord2f(1.0f, 0.0f);
		  glColor3f(terrain[x+1][z][1]/255.0f, terrain[x+1][z][1]/255.0f, 
		  terrain[x+1][z][1]/255.0f);
		  glVertex3f(terrain[x+1][z][0], terrain[x+1][z][1], 
	  	  terrain[x+1][z][2]);

		  glTexCoord2f(0.0f, 1.0f);
		  glColor3f(terrain[x][z+1][1]/255.0f, terrain[x][z+1][1]/255.0f, 
		  terrain[x][z+1][1]/255.0f);
		  glVertex3f(terrain[x][z+1][0], terrain[x][z+1][1], 
		  terrain[x][z+1][2]);

		  glColor3f(terrain[x+1][z+1][1]/255.0f, 
		  terrain[x+1][z+1][1]/255.0f, terrain[x+1][z+1][1]/255.0f);
		  glTexCoord2f(1.0f, 1.0f);
		  glVertex3f(terrain[x+1][z+1][0], terrain[x+1][z+1][1], 
		  terrain[x+1][z+1][2]);
		}
		glEnd();
	}
	glFlush();
	SwapBuffers(g_HDC);

}
Dan zijn we nu aan het renderen van het heightfield aangekomen wat eigenlijk niet meer zo'n probleem moet zijn, we hebben de coordinaten immers al in een array staan. Zoals je ziet zit er ook een color component in die de hoger gelegen gebieden lichter maakt dan de lager gelegen gebieden. De waardes die je hier in zet zijn gewoon de y waardes gedeelt door 255 grijswaarden.

////////////////////////////////////////////////////////////////////////////
//laden van textures
////////////////////////////////////////////////////////////////////////////
bool loadtextures()
{
	landtexture=loadbmp("land.bmp", &landinfo);
	
	glGenTextures(1, &land);
	glBindTexture(GL_TEXTURE_2D, land);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGB, landinfo.biWidth, 
	landinfo.biHeight, GL_RGB, GL_UNSIGNED_BYTE, landtexture);

	return true;
}

////////////////////////////////////////////////////////////////////////////
//initialisatie van opengl
////////////////////////////////////////////////////////////////////////////
void initgl()
{
	glClearColor(0.0f,0.0f,0.0f,0.0f);
	glShadeModel(GL_SMOOTH);
	glEnable(GL_DEPTH_TEST);
	glEnable(GL_CULL_FACE);
	glEnable(GL_TEXTURE_2D);
	
	loadtextures();

	imagedata = loadbmp("heightmap.bmp", &bmpinfo);

	InitializeTerrain();
	loadtextures();
	
}
Het laden van de textures en de initialisatie van OpenGL spreken voor zich en is ook een mooie plek om de heightmap te laden en het terrein te initializeren.


//////////////////////////////////////////////////////////////////////////
//Pixelformatdescriptor
/////////////////////////////////////////////////////////////////////////

void setuppfd(HDC hDC)
{
	int nPixelFormat;

	static PIXELFORMATDESCRIPTOR pfd={
		sizeof(PIXELFORMATDESCRIPTOR),
		1,
		PFD_DRAW_TO_WINDOW|
		PFD_SUPPORT_OPENGL|
		PFD_DOUBLEBUFFER,
		PFD_TYPE_RGBA,
		32,
		0,0,0,0,0,0,
		0,
		0,
		0,
		0,0,0,0,
		16,
		0,
		0,
		PFD_MAIN_PLANE,
		0,
		0,0,0};

		nPixelFormat = ChoosePixelFormat(hDC, &pfd);
		SetPixelFormat(hDC,nPixelFormat, &pfd);
}

///////////////////////////////////////////////////////////////////////////
//Windows Procedure
///////////////////////////////////////////////////////////////////////////

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM	lParam)	
{
	static HGLRC hRC;
	static HDC hDC;
	int width, height;

	switch (message)
	{
		case WM_CREATE:
			hDC = GetDC(hwnd);
			g_HDC = hDC;
			setuppfd(hDC);

			hRC = wglCreateContext(hDC);
			wglMakeCurrent(hDC,hRC);
			break;

		case WM_CLOSE:
			wglMakeCurrent(hDC,NULL);
			wglDeleteContext(hRC);

			PostQuitMessage(0);
			break;
			
		case WM_SIZE:
			height = HIWORD(lParam);
			width = LOWORD(lParam);

			if (height==0)
			{
				height=1;
			}

			glViewport(0,0,width,height);
			glMatrixMode(GL_PROJECTION);
			glLoadIdentity();

			gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,
			1.0f,1000.0f);

			glMatrixMode(GL_MODELVIEW);
			glLoadIdentity();
			break;
			
		case WM_KEYDOWN:
			switch (wParam)
			{
				case VK_ESCAPE:
					wglMakeCurrent(hDC,NULL);
					wglDeleteContext(hRC);
					PostQuitMessage(0);
					break;
			}

	}
	return DefWindowProc(hwnd,message,wParam,lParam);
}

/////////////////////////////////////////////////////////////////////////////////////
//Main Functie
/////////////////////////////////////////////////////////////////////////////////////
int WINAPI WinMain(HINSTANCE hInstance,	HINSTANCE hPrevInstance,
		   LPSTR lpCmdLine, int	nShowCmd)
{
	WNDCLASSEX	wc;	
	HWND hwnd;
	MSG		msg;
	DWORD dwExStyle, dwStyle;
	bool	done=FALSE;
	RECT windowRect;

	int width=800,height=600,bits=32;
					
	windowRect.top = 0;
	windowRect.left = 0;
	windowRect.bottom = height;
	windowRect.right = width;

	wc.cbSize		= sizeof(WNDCLASSEX);	
	wc.style		= CS_HREDRAW | CS_VREDRAW;
	wc.lpfnWndProc		= WndProc;
	wc.cbClsExtra		= 0;
	wc.cbWndExtra		= 0;
	wc.hInstance		= hInstance;	
	wc.hIcon		= LoadIcon(NULL, IDI_APPLICATION);
	wc.hCursor		= LoadCursor(NULL, IDC_ARROW);
	wc.hbrBackground	= (HBRUSH)GetStockObject(BLACK_BRUSH);	
	wc.lpszMenuName		= NULL;
	wc.lpszClassName	= "OpenGL";
	wc.hIconSm		= LoadIcon(NULL,IDI_WINLOGO);

	RegisterClassEx(&wc);

	if(fullscreen)
	{
	DEVMODE devModeScreen;
	memset(&devModeScreen, 0,sizeof(devModeScreen));
	devModeScreen.dmSize = sizeof(devModeScreen);
	devModeScreen.dmPelsWidth = width;
	devModeScreen.dmPelsHeight = height;
	devModeScreen.dmBitsPerPel = bits;
	devModeScreen.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL;

	ChangeDisplaySettings(&devModeScreen, CDS_FULLSCREEN);

	dwExStyle = WS_EX_APPWINDOW;
	dwStyle = WS_POPUP;
	ShowCursor(FALSE);
	}

	else
	{
		dwExStyle = NULL;
		dwStyle = WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_SYSMENU;
	}

	
	AdjustWindowRectEx(&windowRect, dwStyle, FALSE, dwExStyle);

	hwnd=CreateWindowEx(dwExStyle,
			    "OpenGL",	
			    "glwindow",
			    dwStyle | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, 
			    0,0,
			    windowRect.right - windowRect.left,
			    windowRect.bottom - windowRect.top,
			    NULL,
			    NULL,
			    hInstance,	
			    NULL);
	
	ShowWindow(hwnd, SW_SHOW);
	UpdateWindow(hwnd);
	initgl();
	
	while(!done)
	{
		PeekMessage(&msg,NULL,0,0,PM_REMOVE);	
			
			if (msg.message==WM_QUIT)
			{
				done=TRUE;
			}
			else
			{
				render();
				TranslateMessage(&msg);	
				DispatchMessage(&msg);
			}
	}
	return (msg.wParam);
}
En natuurlijk het window zodat je je mooie creatie op het scherm kunt laten zien.

Succes

Vampire,