|
|
nehe 35课 用高度图绘制地形
freefall
欢迎来到另一个令人激动的教程!本课的代码的作者是Ben Humphrey,它是基于第一课的OpenGL框架。现在你应该已经是一个OpenGL行家了吧{笑},把这些代码移到你的代码中只是一瞬间的事了!
本教程将会教你怎样利用高度图创建看起来很棒的地形。对于那些还不知道什么是高度图的人,我会给出一个比较简单的解释。简单的说高度图就是从一个面上的位移,对于那些还在抓着头问“这个人在讲什么?!”的人…高度图代表了我们地形的低、高点。现在完全由你来决定哪块阴影代表低点,哪块代表高点。还有一点很重要,高度图并不一定是图片…你能够利用任何类型数据创建高度图。例如你能够用声音流来创建并表示一个高度图。如果你还不明白…继续读…当你读完整个教程的时候就会开始变得有意义了。
#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
我们从定义一些很重要的变量开始我们的程序。MAP_SIZE是我们的高度图的尺寸。在这篇教程中图的大小是1024×1024。STEP_SIZE是我们用来绘制地形的正方体的大小,减小步长,地形图就会变得更加平滑。有一点很重要,步长越小你的程序所做的工作就将越多,特别是在使用大的高度图时。HEIGHT_RATIO用来缩放地形的Y轴的值,较小的HEIGHT_RATIO可以用来绘制较平的山,较大的HEIGHT_RATIO用来绘制较高的山。
在下面的代码中,你会看到变量bRender。如果bRender的值为真(默认值),我们就填充模式绘制多边形,如果它的值为假,那么我们就用线框模式绘制地形。
#define MAP_SIZE 1024 // Size Of Our .RAW Height Map ( NEW )
#define STEP_SIZE 16 // Width And Height Of Each Quad ( NEW )
#define HEIGHT_RATIO 1.5f // Ratio That The Y Is Scaled According To The X And Z ( NEW )
HDC hDC=NULL; // Private GDI Device Context
HGLRC hRC=NULL; // Permanent Rendering Context
HWND hWnd=NULL; // Holds Our Window Handle
HINSTANCE hInstance; // 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 )
这里我们用了一个字节数组(g_HeightMap[ ])来存储我们的高度图数据。储存在.RAW文件中的高度图数据的值的范围是从0到255,我们可以利用这些值作为我们的高度数据,255是最高点,而0是最低点。我们还有一个名为scaleValue的变量用来缩放我们的整个场景。它可以让使用者自己来缩小和放大场景。
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
ReSizeGLScene()中的代码和第一课中的完全相同,除了最远距离从100.0f 改为了500.0f。
GLvoid ReSizeGLScene(GLsizei width, GLsizei height) // Resize And Initialize The GL Window
{
... CUT ...
}
接下来的这段代码是用来读入.RAW文件的。并不复杂!我们把文件以 读/二进制 的方式打开。接下来我们检查文件是否被找到和是否能被打开。如果不能打开文件或遇到其它的问题,将会显示错误提示消息。
// 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;
}
到这里的时候,我们可以认为文件已经被打开了。现在我们可以读取数据了。我们用fread()来完成这项工作。PheightMap表示的是我们存储数据的位置(即指向g_Heightmap数组的指针)。1代表了要读入多少个数据(每次一个字节),nSize是要读入数据的最大数量(图片大小的字节数---图片的宽度×图片的高度)。最后pFile是指向我们结构体的指针。
把数据读入后,我们检查结果看是否发生了错误。我们把结果存在result变量中并检查它的值,如果确实发生了错误,那我们就弹出一条错误信息。
最后我们要做的事是用fclose(pFile)把文件关闭。
// 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);
}
初始化代码是相当基础的。我们把背景色清除设置成了黑色,打开深度测试、多边型滤波等等。在这些之后我们导入了我们的.RAW文件。我们把文件名("Data/Terrain.raw")、.RAW文件的大小(MAP_SIZE * MAP_SIZE)和高度图数组(g_HeightMap)传递给函数LoadRawFile()。这涉及到了上边导入.RAW的代码。.RAW文件会被读入,数据会被存储在高度图数组中(g_HeightMap)。
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
}
这是用来做高度图数组的索引。无论什么时候我们处理数组的时候,我们都必须确信数组没有越界。为了确定越界是否发生我们使用了%,%可以阻止我们的X值或Y值超过MAX_SIZE ? 1。
我们检查pHeightMap是否指向了正确的数据,如果没有,那么我们返回一个0。否则,返回下标为X ,Y处高度图的值,现在你应该明白为什么我们要用Y×MAP_SIZE移到所需的数据处了(相当于二维数组),接着往下看吧!
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
我们需要把整个一维的数组看成是一个二维数组。可以利用这个表达式:下标= (x + (y * arrayWidth) )。我们假设把它看成是这样的:pHeightMap[x][y],否则也可以用相反的:(y + (x * arrayWidth) )。
现在我们有了正确的数组下标了,就可以返回在该处的高度了(数组在x y 处的数据)。
return pHeightMap[x + (y * MAP_SIZE)]; // Index Into Our Height Array And Return The Height
}
这里我们根据高度来为每个点设置颜色。为了让它变得暗一点,我们从-0.15f开始。我们根据高度/256来获得颜色的比例。如果没有数据,那么函数就不设置颜色直接返回。如果一切都正常的话,我们用glColor3f(0.0f, fColor, 0.0f)来设置蓝色的深度,你可以试着把fColor移到红色或蓝色来改变地形的颜色。
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 );
}
这部分代码是真正的在绘制地形。X和Y用来遍历整个高度数据。X,Y,Z用来绘制组成大地形的小方块。
就象我们经常所做的那样,我们检查高度图(pHeightMap)是否包含了数据。如果没有,我们直接返回什么也不做。
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 = True,那么我们就要绘制多边形,否则只绘制线条。
if(bRender) // What We Want To Render
glBegin( GL_QUADS ); // Render Polygons
else
glBegin( GL_LINES ); // Render Lines Instead
下一步我们要做的是根据高度图来绘制地形。我们读入并选取了高度图中的一些数据来绘制我们的点。如果我们能看见整个过程是这样发生的话,那么就是先绘制了列(Y),然后绘制行。注意我们有一个STEP_SIZE变量。它决定了高度图的细节,它的值越大,地形看起来就越是成块状的,它的值越小,地形就越平滑。如果我们把STEP_SIZE的值设为1。那么高度图中每个象素都会被当成一个点来绘制。我选择16做为一个恰当的值,太多的点会导致速度很慢。当你打开光照的时候你可以增加点的数量。点的光照效果能够掩饰地形的不光滑。在这里我们不用光照,为了简化教程我们仅仅给每个点赋予不同的颜色值。多边形位置越高它的颜色就越亮。
for ( X = 0; X < MAP_SIZE; X += STEP_SIZE )
for ( Y = 0; Y < MAP_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();
我们做完这些后,我们把颜色恢复成亮白色,并且把alpha值设置成1.0f。如果还有其它的物体在屏幕上,我们并不想让他们的颜色也变成蓝色:
glColor4f(1.0f, 1.0f, 1.0f, 1.0f); // Reset The Color
}
对于那些以前没有用过gluLookAt() 函数的人,这个函数所做的就是设置照相机的位置、观看的方向(view)和向上的矢量(up vector)。在这里我们把相机放在了一个比较好的位置来得到地形的好的外围图。为了避免使用高度数值,我们把地形的点的高度值除以一个放缩常数,就象我们在下边用的函数glScalef()一样。
gluLookAt()的参数如下:前三个参数代表照相机的位置。所以前三个值我们把照相机放在了x=212、y=60、z=194的地方。接下来的三个参数代表了我们想让照相机对着的方向。在这篇教程中,当运行演示程序的时候你会注意到我们看的方向稍稍偏左了一点,并且向下方偏了一点,向左的值是186中心在212上,55比60稍低了一点,给我们的感觉是我们比地形高了一点,并且看它的时候有一点倾斜(在它的顶部看)。171是照相机离物体的距离。最后的三个参数是告诉OpenGL“上”的方向。我们的山的向上的方向是Y轴,所以我们把“上”的方向设为Y=1其它两个值设为0。
在第一次使用gluLookAt的时候你会觉得非常的棘手,在读了上面这些粗略的介绍之后你仍然会比较迷茫。我的一个好的建议就是试着稍微改变一下这些值,改变照相机的位置。如果你要改变照相机的Y值比如说120,你就能看到更多地形的顶部,因为你仍然是从55开始向下看的。
我不知道这对你是否有帮助,但我仍要用一个真实生活的例子来做说明。比如说你的高度有6英尺多一点。让我们继续假设你的眼睛在6英尺处(你的眼睛代表了照相机— 6英尺 在 Y轴上有6个单位)。现在如果你站在一个两英尺高的墙的面前(在Y轴上有2个单位),你能够向下看见墙并且能够看见墙的顶部。如果墙有7英尺高,你能够向上方看见墙但是看不见墙的顶部,观看方向(The view)的改变取决于你看上方或下方(如果你比你所看的物体高/低)。希望这些对你有用!
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
这条语句缩放地图的大小,使得它更容易观看并且不会太大。我们能使用键盘上方向键“上”和“下”来改变scaleValue的值。你会注意到我们用HEIGHT_RATIO乘上Y的scaleValue值。这是为了 让地形更高一点并且有更高的精确度。
glScalef(scaleValue, scaleValue * HEIGHT_RATIO, scaleValue);
如果我们把g_HeightMap 的数据传递给函数RenderHeightMap(),这样就会绘制出一个方块地形。如果你想要使用这个函数,那么就应该把(X, Y)也作为参数来绘制它,或者直接使用 OpenGL的矩阵操作(glTranslatef() glRotate(), 等等)把这块地形放在我们想要的地方。
RenderHeightMap(g_HeightMap); // Render The Height Map
return TRUE; // Keep Going
}
函数KillGLWindow()的代码和第一课是相同的。
GLvoid KillGLWindow(GLvoid) // Properly Kill The Window
{
}
函数CreateGLWindow()的代码也和第一课相同。
BOOL CreateGLWindow(char* title, int width, int height, int bits, bool fullscreenflag)
{
}
函数WndProc()中所做的改变是加上了WM_LBUTTONDOWN,它所做的是检查是否按下了鼠标左键。如果是那么绘制状态就从填充模式变为线框模式,或是从线框模式变为填充模式。
LRESULT CALLBACK WndProc( HWND hWnd, // Handle For This Window
UINT uMsg, // Message For This Window
WPARAM wParam, // Additional Message Information
LPARAM lParam) // Additional Message Information
{
switch (uMsg) // Check For Windows Messages
{
case WM_ACTIVATE: // Watch For Window Activate Message
{
if (!HIWORD(wParam)) // Check Minimization State
{
active=TRUE; // Program Is Active
}
else
{
active=FALSE; // Program Is No Longer Active
}
return 0; // Return To The Message Loop
}
case WM_SYSCOMMAND: // Intercept System Commands
{
switch (wParam) // Check System Calls
{
case SC_SCREENSAVE: // Screensaver Trying To Start?
case SC_MONITORPOWER: // Monitor Trying To Enter Powersave?
return 0; // Prevent From Happening
}
break; // Exit
}
case WM_CLOSE: // Did We Receive A Close Message?
{
PostQuitMessage(0); // Send A Quit Message
return 0; // Jump Back
}
case WM_LBUTTONDOWN: // Did We Receive A Left Mouse Click?
{
bRender = !bRender; // Change Rendering State Between Fill/Wire Frame
return 0; // Jump Back
}
case WM_KEYDOWN: // Is A Key Being Held Down?
{
keys[wParam] = TRUE; // If So, Mark It As TRUE
return 0; // Jump Back
}
case WM_KEYUP: // Has A Key Been Released?
{
keys[wParam] = FALSE; // If So, Mark It As FALSE
return 0; // Jump Back
}
case WM_SIZE: // Resize The OpenGL Window
{
ReSizeGLScene(LOWORD(lParam),HIWORD(lParam)); // LoWord=Width, HiWord=Height
return 0; // Jump Back
}
}
// Pass All Unhandled Messages To DefWindowProc
return DefWindowProc(hWnd,uMsg,wParam,lParam);
}
这部分代码没有什么大的改变。最显著的变化是窗口的名称。在检查按键状态之前的其它代码都是相同的。
int WINAPI WinMain( HINSTANCE hInstance, // Instance
HINSTANCE hPrevInstance, // Previous Instance
LPSTR lpCmdLine, // Command Line Parameters
int nCmdShow) // Window Show State
{
MSG msg; // Windows Message Structure
BOOL done=FALSE; // Bool Variable To Exit Loop
// Ask The User Which Screen Mode They Prefer
if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)
{
fullscreen=FALSE; // Windowed Mode
}
// Create Our OpenGL Window
if (!CreateGLWindow("NeHe & Ben Humphrey's Height Map Tutorial", 640, 480, 16, fullscreen))
{
return 0; // Quit If Window Was Not Created
}
while(!done) // Loop That Runs While done=FALSE
{
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Is There A Message Waiting?
{
if (msg.message==WM_QUIT) // Have We Received A Quit Message?
{
done=TRUE; // If So done=TRUE
}
else // If Not, Deal With Window Messages
{
TranslateMessage(&msg); // Translate The Message
DispatchMessage(&msg); // Dispatch The Message
}
}
else // If There Are No Messages
{
// Draw The Scene. Watch For ESC Key And Quit Messages From DrawGLScene()
if ((active && !DrawGLScene()) || keys[VK_ESCAPE]) // Active? Was There A Quit Received?
{
done=TRUE; // ESC or DrawGLScene Signalled A Quit
}
else if (active) // Not Time To Quit, Update Screen
{
SwapBuffers(hDC); // Swap Buffers (Double Buffering)
}
if (keys[VK_F1]) // Is F1 Being Pressed?
{
keys[VK_F1]=FALSE; // If So Make Key FALSE
KillGLWindow(); // Kill Our Current Window
fullscreen=!fullscreen; // Toggle Fullscreen / Windowed Mode
// Recreate Our OpenGL Window
if (!CreateGLWindow("NeHe & Ben Humphrey's Height Map Tutorial", 640, 480, 16, fullscreen))
{
return 0; // Quit If Window Was Not Created
}
}
下面这段代码能够让你增加和减少scaleValue的值。按下”up”键的时候,scaleValue的值增加,地图变得大些。按下”down”键的时候,scaleValue的值减少,地图变得小一些。
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
}
这就是整个由高度图来创建漂亮的地形图的全过程。我希望你欣赏Ben的成果!就象往常一样,如果你发现了教程或是代码的错误,请发电子邮件给我,我会努力的纠正错误/修订教程。
一旦你明白了代码是怎样工作的,试着去改变它。你可以试着增加一个在表面上旋转的小球。你已经知道了地形上每个部分的高度了,因此增加一个球应该没有什么问题。你还可以试着手工创建高度图,使得它成为一个卷轴地形,给地形增加代表被雪覆盖的山峰/水/等的颜色,加上纹理,添加乳化效果(plasma effect)来创造一个不断变化的地形。有无穷种可能性。
希望你能喜欢这篇教程!你可以访问Ben的网站
http://www.GameTutorials.com.
|
|