NeHe OpenGL第十課:3D世界


加載3D世界,并在其中漫游:
在這一課中,你將學(xué)會如何加載3D世界,并在3D世界中漫游。這一課使用第一課的代碼,當(dāng)然在課程說明中我只介紹改變了代碼。
這一課是由Lionel Brits (βtelgeuse)所寫的。在本課中我們只對增加的代碼做解釋。當(dāng)然只添加課程中所寫的代碼,程序是不會運(yùn)行的。如果您有興趣知道下面的每一行代碼是如何運(yùn)行的話,請下載完整的源碼
,并在瀏覽這一課的同時,對源碼進(jìn)行跟蹤。
好了現(xiàn)在歡迎來到名不見經(jīng)傳的第十課。到現(xiàn)在為止,您應(yīng)該有能力創(chuàng)建一個旋轉(zhuǎn)的立方體或一群星星了,對3D編程也應(yīng)該有些感覺了吧?但還是請等一下!不要立馬沖動地要開始寫個Quake
IV,好不好...:)。只靠旋轉(zhuǎn)的立方體還很難來創(chuàng)造一個可以決一死戰(zhàn)的酷斃了的對手....:)?,F(xiàn)在這些日子您所需要的是一個大一點(diǎn)的、更復(fù)雜些的、動態(tài)3D世界,它帶有空間的六自由度和花哨的效果如鏡像、入口
、扭曲等等,當(dāng)然還要有更快的幀顯示速度。這一課就要解釋一個基本的3D世界"結(jié)構(gòu)",以及如何在這個世界里游走。
數(shù)據(jù)結(jié)構(gòu)
當(dāng)您想要使用一系列的數(shù)字來完美的表達(dá)3D環(huán)境時,隨著環(huán)境復(fù)雜度的上升,這個工作的難度也會隨之上升。出于這個原因,我們必須將數(shù)據(jù)歸類,使其具有更多的可操作性風(fēng)格。在程序清單頭部出現(xiàn)了sector(區(qū)段)
的定義。每個3D世界基本上可以看作是sector(區(qū)段)的集合。一個sector(區(qū)段)可以是一個房間、一個立方體、或者任意一個閉合的區(qū)間。
typedef struct tagSECTOR // 創(chuàng)建Sector區(qū)段結(jié)構(gòu)
{
int numtriangles; // Sector中的三角形個數(shù)
TRIANGLE* triangle; // 指向三角數(shù)組的指針
} SECTOR; // 命名為SECTOR
一個sector(區(qū)段)包含了一系列的多邊形,所以下一個目標(biāo)就是triangle(我們將只用三角形,這樣寫代碼更容易些)。
typedef struct tagTRIANGLE // 創(chuàng)建Triangle三角形結(jié)構(gòu)
{
VERTEX vertex[3]; // VERTEX矢量數(shù)組,大小為3
} TRIANGLE; // 命名為 TRIANGLE
三角形本質(zhì)上是由一些(兩個以上)頂點(diǎn)組成的多邊形,頂點(diǎn)同時也是我們的最基本的分類單位。頂點(diǎn)包含了OpenGL真正感興趣的數(shù)據(jù)。我們用3D空間中的坐標(biāo)值(x,y,z)以及它們的紋理坐標(biāo)(u,v)來定義三角形的每
個頂點(diǎn)。
typedef struct tagVERTEX // 創(chuàng)建Vertex頂點(diǎn)結(jié)構(gòu)
{
float x, y, z; // 3D 坐標(biāo)
float u, v; // 紋理坐標(biāo)
} VERTEX; // 命名為VERTEX
載入文件
在程序內(nèi)部直接存儲數(shù)據(jù)會讓程序顯得太過死板和無趣。從磁盤上載入世界資料,會給我們帶來更多的彈性,可以讓我們體驗不同的世界,而不用被迫重新編譯程序。另一個好處就是用戶可以切換世界資料并修改它們而
無需知道程序如何讀入輸出這些資料的。數(shù)據(jù)文件的類型我們準(zhǔn)備使用文本格式。這樣編輯起來更容易,寫的代碼也更少。等將來我們也許會使用二進(jìn)制文件。
問題是,怎樣才能從文件中取得數(shù)據(jù)資料呢?首先,創(chuàng)建一個叫做SetupWorld()的新函數(shù)。把這個文件定義為filein,并且使用只讀方式打開文件。我們必須在使用完畢之后關(guān)閉文件。大家一起來看看現(xiàn)在的代碼:
// 先前的定義: char* worldfile = "data\\world.txt";
void SetupWorld() // 設(shè)置我們的世界
{
FILE *filein; // 工作文件
filein = fopen(worldfile, "rt"); // 打開文件
...
(讀入數(shù)據(jù)資料))
...
fclose(filein); // 關(guān)閉文件
return; // 返回
}
下一個挑戰(zhàn)是將每個單獨(dú)的文本行讀入變量。這有很多辦法可以做到。一個問題是文件中并不是所有的行都包含有意義的信息??招泻妥⑨尣粦?yīng)該被讀入。我們創(chuàng)建了一個叫做readstr()的函數(shù)。這個函數(shù)會從數(shù)據(jù)文
件中讀入一個有意義的行至一個已經(jīng)初始化過的字符串。下面就是代碼:
void readstr(FILE *f,char *string) // 讀入一個字符串
{
do // 循環(huán)開始
{
fgets(string, 255, f); // 讀入一行
} while ((string[0] == '/') || (string[0] == '\n')); // 考察是否有必要進(jìn)行處理
return; // 返回
}
下一步我們讀入?yún)^(qū)段數(shù)據(jù)。這一課將只處理一個區(qū)段,不過實(shí)現(xiàn)一個多區(qū)段引擎也很容易。讓我們將注意力轉(zhuǎn)回SetupWorld()。程序必須知道區(qū)段內(nèi)包含了多少個三角形。我們在數(shù)據(jù)文件中以下面這種形式定義三角形
數(shù)量:
接下來是讀取三角形數(shù)量的代碼:
int numtriangles; // 區(qū)段中的三角形數(shù)量
char oneline[255]; // 存儲數(shù)據(jù)的字符串
...
readstr(filein,oneline); // 讀入一行數(shù)據(jù)
sscanf(oneline, "NUMPOLLIES %d\n", &numtriangles); // 讀入三角形數(shù)量
余下的世界載入過程采用了相似的方法。接著,我們對區(qū)段進(jìn)行初始化,并讀入部分?jǐn)?shù)據(jù):
// 先前的定義: SECTOR sector1;
char oneline[255]; // 存儲數(shù)據(jù)的字符串
int numtriangles; // 區(qū)段的三角形數(shù)量
float x, y, z, u, v; // 3D 和 紋理坐標(biāo)
...
sector1.triangle = new TRIANGLE[numtriangles]; // 為numtriangles個三角形分配內(nèi)存并設(shè)定指針
sector1.numtriangles = numtriangles; // 定義區(qū)段1中的三角形數(shù)量
// 遍歷區(qū)段中的每個三角形
for (int triloop = 0; triloop < numtriangles; triloop++) // 遍歷所有的三角形
{
// 遍歷三角形的每個頂點(diǎn)
for (int vertloop = 0; vertloop < 3; vertloop++) // 遍歷所有的頂點(diǎn)
{
readstr(filein,oneline); // 讀入一行數(shù)據(jù)
// 讀入各自的頂點(diǎn)數(shù)據(jù)
sscanf(oneline, "%f %f %f %f %f", &x, &y, &z, &u, &v);
// 將頂點(diǎn)數(shù)據(jù)存入各自的頂點(diǎn)
sector1.triangle[triloop].vertex[vertloop].x = x; // 區(qū)段 1, 第 triloop 個三角形, 第 vertloop 個頂點(diǎn), 值 x =x
sector1.triangle[triloop].vertex[vertloop].y = y; // 區(qū)段 1, 第 triloop 個三角形, 第 vertloop 個頂點(diǎn), 值 y =y
sector1.triangle[triloop].vertex[vertloop].z = z; // 區(qū)段 1, 第 triloop 個三角形, 第 vertloop 個頂點(diǎn), 值 z =z
sector1.triangle[triloop].vertex[vertloop].u = u; // 區(qū)段 1, 第 triloop 個三角形, 第 vertloop 個頂點(diǎn), 值 u =u
sector1.triangle[triloop].vertex[vertloop].v = v; // 區(qū)段 1, 第 triloop 個三角形, 第 vertloop 個頂點(diǎn), 值 e=v
}
}
數(shù)據(jù)文件中每個三角形都以如下形式聲明:
X1 Y1 Z1 U1 V1
X2 Y2 Z2 U2 V2
X3 Y3 Z3 U3 V3
顯示世界
現(xiàn)在區(qū)段已經(jīng)載入內(nèi)存,我們下一步要在屏幕上顯示它。到目前為止,我們所作過的都是些簡單的旋轉(zhuǎn)和平移。但我們的鏡頭始終位于原點(diǎn)(0,0,0)處。任何一個不錯的3D引擎都會允許用戶在這個世界中游走和遍歷,
我們的這個也一樣。實(shí)現(xiàn)這個功能的一種途徑是直接移動鏡頭并繪制以鏡頭為中心的3D環(huán)境。這樣做會很慢并且不易用代碼實(shí)現(xiàn)。我們的解決方法如下:
根據(jù)用戶的指令旋轉(zhuǎn)并變換鏡頭位置。
圍繞原點(diǎn),以與鏡頭相反的旋轉(zhuǎn)方向來旋轉(zhuǎn)世界。(讓人產(chǎn)生鏡頭旋轉(zhuǎn)的錯覺)
以與鏡頭平移方式相反的方式來平移世界(讓人產(chǎn)生鏡頭移動的錯覺)。
這樣實(shí)現(xiàn)起來就很簡單.
下面從第一步開始吧(平移并旋轉(zhuǎn)鏡頭)。
if (keys[VK_RIGHT]) // 右方向鍵按下了么?
{
yrot -= 1.5f; // 向左旋轉(zhuǎn)場景
}
if (keys[VK_LEFT]) // 左方向鍵按下了么?
{
yrot += 1.5f; // 向右側(cè)旋轉(zhuǎn)場景
}
if (keys[VK_UP]) // 向上方向鍵按下了么?
{
xpos -= (float)sin(heading*piover180) * 0.05f; // 沿游戲者所在的X平面移動
zpos -= (float)cos(heading*piover180) * 0.05f; // 沿游戲者所在的Z平面移動
if (walkbiasangle >= 359.0f) // 如果walkbiasangle大于359度
{
walkbiasangle = 0.0f; // 將 walkbiasangle 設(shè)為0
}
else // 否則
{
walkbiasangle+= 10; // 如果 walkbiasangle < 359 ,則增加 10
}
walkbias = (float)sin(walkbiasangle * piover180)/20.0f; // 使游戲者產(chǎn)生跳躍感
}
if (keys[VK_DOWN]) // 向下方向鍵按下了么?
{
xpos += (float)sin(heading*piover180) * 0.05f; // 沿游戲者所在的X平面移動
zpos += (float)cos(heading*piover180) * 0.05f; // 沿游戲者所在的Z平面移動
if (walkbiasangle <= 1.0f) // 如果walkbiasangle小于1度
{
walkbiasangle = 359.0f; // 使 walkbiasangle 等于 359
}
else // 否則
{
walkbiasangle-= 10; // 如果 walkbiasangle > 1 減去 10
}
walkbias = (float)sin(walkbiasangle * piover180)/20.0f; // 使游戲者產(chǎn)生跳躍感
}
這個實(shí)現(xiàn)很簡單。當(dāng)左右方向鍵按下后,旋轉(zhuǎn)變量yrot
相應(yīng)增加或減少。當(dāng)前后方向鍵按下后,我們使用sine和cosine函數(shù)重新生成鏡頭位置(您需要些許三角函數(shù)學(xué)的知識:-)。Piover180
是一個很簡單的折算因子用來折算度和弧度。
接著您可能會問:walkbias是什么意思?這是NeHe的發(fā)明的單詞:-)?;旧暇褪钱?dāng)人行走時頭部產(chǎn)生上下擺動的幅度。我們使用簡單的sine正弦波來調(diào)節(jié)鏡頭的Y軸位置。如果不添加這個而只是前后移動的話,程序
看起來就沒這么棒了。
現(xiàn)在,我們已經(jīng)有了下面這些變量??梢蚤_始進(jìn)行步驟2和3了。由于我們的程序還不太復(fù)雜,我們無需新建一個函數(shù),而是直接在顯示循環(huán)中完成這些步驟。
int DrawGLScene(GLvoid) // 繪制 OpenGL 場景
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清除 場景 和 深度緩沖
glLoadIdentity(); // 重置當(dāng)前矩陣
GLfloat x_m, y_m, z_m, u_m, v_m; // 頂點(diǎn)的臨時 X, Y, Z, U 和 V 的數(shù)值
GLfloat xtrans = -xpos; // 用于游戲者沿X軸平移時的大小
GLfloat ztrans = -zpos; // 用于游戲者沿Z軸平移時的大小
GLfloat ytrans = -walkbias-0.25f; // 用于頭部的上下擺動
GLfloat sceneroty = 360.0f - yrot; // 位于游戲者方向的360度角
int numtriangles; // 保有三角形數(shù)量的整數(shù)
glRotatef(lookupdown,1.0f,0,0); // 上下旋轉(zhuǎn)
glRotatef(sceneroty,0,1.0f,0); // 根據(jù)游戲者正面所對方向所作的旋轉(zhuǎn)
glTranslatef(xtrans, ytrans, ztrans); // 以游戲者為中心的平移場景
glBindTexture(GL_TEXTURE_2D, texture[filter]); // 根據(jù) filter 選擇的紋理
numtriangles = sector1.numtriangles; // 取得Sector1的三角形數(shù)量
// 逐個處理三角形
for (int loop_m = 0; loop_m < numtriangles; loop_m++) // 遍歷所有的三角形
{
glBegin(GL_TRIANGLES); // 開始繪制三角形
glNormal3f( 0.0f, 0.0f, 1.0f); // 指向前面的法線
x_m = sector1.triangle[loop_m].vertex[0].x; // 第一點(diǎn)的 X 分量
y_m = sector1.triangle[loop_m].vertex[0].y; // 第一點(diǎn)的 Y 分量
z_m = sector1.triangle[loop_m].vertex[0].z; // 第一點(diǎn)的 Z 分量
u_m = sector1.triangle[loop_m].vertex[0].u; // 第一點(diǎn)的 U 紋理坐標(biāo)
v_m = sector1.triangle[loop_m].vertex[0].v; // 第一點(diǎn)的 V 紋理坐標(biāo)
glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m); // 設(shè)置紋理坐標(biāo)和頂點(diǎn)
x_m = sector1.triangle[loop_m].vertex[1].x; // 第二點(diǎn)的 X 分量
y_m = sector1.triangle[loop_m].vertex[1].y; // 第二點(diǎn)的 Y 分量
z_m = sector1.triangle[loop_m].vertex[1].z; // 第二點(diǎn)的 Z 分量
u_m = sector1.triangle[loop_m].vertex[1].u; // 第二點(diǎn)的 U 紋理坐標(biāo)
v_m = sector1.triangle[loop_m].vertex[1].v; // 第二點(diǎn)的 V 紋理坐標(biāo)
glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m); // 設(shè)置紋理坐標(biāo)和頂點(diǎn)
x_m = sector1.triangle[loop_m].vertex[2].x; // 第三點(diǎn)的 X 分量
y_m = sector1.triangle[loop_m].vertex[2].y; // 第三點(diǎn)的 Y 分量
z_m = sector1.triangle[loop_m].vertex[2].z; // 第三點(diǎn)的 Z 分量
u_m = sector1.triangle[loop_m].vertex[2].u; // 第二點(diǎn)的 U 紋理坐標(biāo)
v_m = sector1.triangle[loop_m].vertex[2].v; // 第二點(diǎn)的 V 紋理坐標(biāo)
glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m); // 設(shè)置紋理坐標(biāo)和頂點(diǎn)
glEnd(); // 三角形繪制結(jié)束
}
return TRUE; // 返回
}
原文及其個版本源代碼下載:
http://nehe./data/lessons/lesson.asp?lesson=10
|