天天看點

NeHe OpenGL教程 第十課:3D世界資料結構載入檔案顯示世界

前言

NeHe OpenGL第十課:3D世界

NeHe OpenGL教程 第十課:3D世界資料結構載入檔案顯示世界

加載3D世界,并在其中漫遊:

在這一課中,你将學會如何加載3D世界,并在3D世界中漫遊。這一課使用第一課的代碼,當然在課程說明中我隻介紹改變了代碼。

這一課是由Lionel Brits (βtelgeuse)所寫的。在本課中我們隻對增加的代碼做解釋。當然隻添加課程中所寫的代碼,程式是不會運作的。如果您有興趣知道下面的每一行代碼是如何運作的話,請下載下傳完整的源碼

,并在浏覽這一課的同時,對源碼進行跟蹤。

好了現在歡迎來到名不見經傳的第十課。到現在為止,您應該有能力建立一個旋轉的立方體或一群星星了,對3D程式設計也應該有些感覺了吧?但還是請等一下!不要立馬沖動地要開始寫個Quake

IV,好不好...:)。隻靠旋轉的立方體還很難來創造一個可以決一死戰的酷斃了的對手....:)。現在這些日子您所需要的是一個大一點的、更複雜些的、動态3D世界,它帶有空間的六自由度和花哨的效果如鏡像、入口

、扭曲等等,當然還要有更快的幀顯示速度。這一課就要解釋一個基本的3D世界"結構",以及如何在這個世界裡遊走。

當您想要使用一系列的數字來完美的表達3D環境時,随着環境複雜度的上升,這個工作的難度也會随之上升。出于這個原因,我們必須将資料歸類,使其具有更多的可操作性風格。在程式清單頭部出現了sector(區段)

的定義。每個3D世界基本上可以看作是sector(區段)的集合。一個sector(區段)可以是一個房間、一個立方體、或者任意一個閉合的區間。 

typedef struct tagSECTOR      // 建立Sector區段結構

{

 int numtriangles;      // Sector中的三角形個數

 TRIANGLE* triangle;      // 指向三角數組的指針

} SECTOR;        // 命名為SECTOR

一個sector(區段)包含了一系列的多邊形,是以下一個目标就是triangle(我們将隻用三角形,這樣寫代碼更容易些)。  

typedef struct tagTRIANGLE      // 建立Triangle三角形結構

 VERTEX vertex[3];      // VERTEX矢量數組,大小為3

} TRIANGLE;        // 命名為 TRIANGLE

三角形本質上是由一些(兩個以上)頂點組成的多邊形,頂點同時也是我們的最基本的分類機關。頂點包含了OpenGL真正感興趣的資料。我們用3D空間中的坐标值(x,y,z)以及它們的紋理坐标(u,v)來定義三角形的每

個頂點。  

typedef struct tagVERTEX      // 建立Vertex頂點結構

 float x, y, z;       // 3D 坐标

 float u, v;       // 紋理坐标

} VERTEX;        // 命名為VERTEX

在程式内部直接存儲資料會讓程式顯得太過死闆和無趣。從磁盤上載入世界資料,會給我們帶來更多的彈性,可以讓我們體驗不同的世界,而不用被迫重新編譯程式。另一個好處就是使用者可以切換世界資料并修改它們而

無需知道程式如何讀入輸出這些資料的。資料檔案的類型我們準備使用文本格式。這樣編輯起來更容易,寫的代碼也更少。等将來我們也許會使用二進制檔案。

問題是,怎樣才能從檔案中取得資料資料呢?首先,建立一個叫做SetupWorld()的新函數。把這個檔案定義為filein,并且使用隻讀方式打開檔案。我們必須在使用完畢之後關閉檔案。大家一起來看看現在的代碼:

// 先前的定義: char* worldfile = "data\\world.txt";

void SetupWorld()       // 設定我們的世界

 FILE *filein;       // 工作檔案

 filein = fopen(worldfile, "rt");    // 打開檔案

 ...

 (讀入資料資料))

 fclose(filein);       // 關閉檔案

 return;        // 傳回

}

下一個挑戰是将每個單獨的文本行讀入變量。這有很多辦法可以做到。一個問題是檔案中并不是所有的行都包含有意義的資訊。空行和注釋不應該被讀入。我們建立了一個叫做readstr()的函數。這個函數會從資料文

件中讀入一個有意義的行至一個已經初始化過的字元串。下面就是代碼:

void readstr(FILE *f,char *string)     //  讀入一個字元串

 do        // 循環開始

 {

  fgets(string, 255, f);     // 讀入一行

 } while ((string[0] == '/') || (string[0] == '\n'));  // 考察是否有必要進行處理

下一步我們讀入區段資料。這一課将隻處理一個區段,不過實作一個多區段引擎也很容易。讓我們将注意力轉回SetupWorld()。程式必須知道區段内包含了多少個三角形。我們在資料檔案中以下面這種形式定義三角形

數量:

接下來是讀取三角形數量的代碼:  

int numtriangles;       // 區段中的三角形數量

char oneline[255];       // 存儲資料的字元串

...

readstr(filein,oneline);      // 讀入一行資料

sscanf(oneline, "NUMPOLLIES %d\n", &numtriangles);   // 讀入三角形數量

餘下的世界載入過程采用了相似的方法。接着,我們對區段進行初始化,并讀入部分資料:  

// 先前的定義: SECTOR sector1;

int numtriangles;       // 區段的三角形數量

float x, y, z, u, v;       // 3D 和 紋理坐标

sector1.triangle = new TRIANGLE[numtriangles];    // 為numtriangles個三角形配置設定記憶體并設定指針

sector1.numtriangles = numtriangles;     // 定義區段1中的三角形數量

// 周遊區段中的每個三角形

for (int triloop = 0; triloop < numtriangles; triloop++)  // 周遊所有的三角形

 // 周遊三角形的每個頂點

 for (int vertloop = 0; vertloop < 3; vertloop++)  // 周遊所有的頂點

  readstr(filein,oneline);    // 讀入一行資料

  // 讀入各自的頂點資料

  sscanf(oneline, "%f %f %f %f %f", &x, &y, &z, &u, &v);

  // 将頂點資料存入各自的頂點

  sector1.triangle[triloop].vertex[vertloop].x = x; // 區段 1,  第 triloop 個三角形, 第  vertloop 個頂點, 值 x =x

  sector1.triangle[triloop].vertex[vertloop].y = y; // 區段 1,  第 triloop 個三角形, 第  vertloop 個頂點, 值 y =y

  sector1.triangle[triloop].vertex[vertloop].z = z; // 區段 1,  第 triloop 個三角形, 第  vertloop 個頂點, 值  z =z

  sector1.triangle[triloop].vertex[vertloop].u = u; // 區段 1,  第 triloop 個三角形, 第  vertloop 個頂點, 值  u =u

  sector1.triangle[triloop].vertex[vertloop].v = v; // 區段 1,  第 triloop 個三角形, 第  vertloop 個頂點, 值  e=v

 }

資料檔案中每個三角形都以如下形式聲明:

X1 Y1 Z1 U1 V1

X2 Y2 Z2 U2 V2

X3 Y3 Z3 U3 V3

現在區段已經載入記憶體,我們下一步要在螢幕上顯示它。到目前為止,我們所作過的都是些簡單的旋轉和平移。但我們的鏡頭始終位于原點(0,0,0)處。任何一個不錯的3D引擎都會允許使用者在這個世界中遊走和周遊,

我們的這個也一樣。實作這個功能的一種途徑是直接移動鏡頭并繪制以鏡頭為中心的3D環境。這樣做會很慢并且不易用代碼實作。我們的解決方法如下: 

根據使用者的指令旋轉并變換鏡頭位置。 

圍繞原點,以與鏡頭相反的旋轉方向來旋轉世界。(讓人産生鏡頭旋轉的錯覺) 

以與鏡頭平移方式相反的方式來平移世界(讓人産生鏡頭移動的錯覺)。 

這樣實作起來就很簡單.

下面從第一步開始吧(平移并旋轉鏡頭)。

if (keys[VK_RIGHT])       // 右方向鍵按下了麼?

 yrot -= 1.5f;       // 向左旋轉場景

if (keys[VK_LEFT])       // 左方向鍵按下了麼?

 yrot += 1.5f;       // 向右側旋轉場景

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 設為0

 else        // 否則

   walkbiasangle+= 10;     // 如果 walkbiasangle < 359 ,則增加 10

 walkbias = (float)sin(walkbiasangle * piover180)/20.0f;  // 使遊戲者産生跳躍感

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

  walkbiasangle-= 10;     // 如果 walkbiasangle > 1 減去 10

這個實作很簡單。當左右方向鍵按下後,旋轉變量yrot

相應增加或減少。目前後方向鍵按下後,我們使用sine和cosine函數重新生成鏡頭位置(您需要些許三角函數學的知識:-)。Piover180

是一個很簡單的折算因子用來折算度和弧度。

接着您可能會問:walkbias是什麼意思?這是NeHe的發明的單詞:-)。基本上就是當人行走時頭部産生上下擺動的幅度。我們使用簡單的sine正弦波來調節鏡頭的Y軸位置。如果不添加這個而隻是前後移動的話,程式

看起來就沒這麼棒了。

現在,我們已經有了下面這些變量。可以開始進行步驟2和3了。由于我們的程式還不太複雜,我們無需建立一個函數,而是直接在顯示循環中完成這些步驟。  

int DrawGLScene(GLvoid)       // 繪制 OpenGL 場景

 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  // 清除 場景 和 深度緩沖

 glLoadIdentity();      // 重置目前矩陣

 GLfloat x_m, y_m, z_m, u_m, v_m;    // 頂點的臨時 X, Y, Z, U 和 V 的數值

 GLfloat xtrans = -xpos;      // 用于遊戲者沿X軸平移時的大小

 GLfloat ztrans = -zpos;      // 用于遊戲者沿Z軸平移時的大小

 GLfloat ytrans = -walkbias-0.25f;    // 用于頭部的上下擺動

 GLfloat sceneroty = 360.0f - yrot;    // 位于遊戲者方向的360度角

 int numtriangles;      // 保有三角形數量的整數

 glRotatef(lookupdown,1.0f,0,0);     // 上下旋轉

 glRotatef(sceneroty,0,1.0f,0);     // 根據遊戲者正面所對方向所作的旋轉

 glTranslatef(xtrans, ytrans, ztrans);    // 以遊戲者為中心的平移場景

 glBindTexture(GL_TEXTURE_2D, texture[filter]);   // 根據 filter 選擇的紋理

 numtriangles = sector1.numtriangles;    // 取得Sector1的三角形數量

 // 逐個處理三角形

 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; // 第一點的 X 分量

   y_m = sector1.triangle[loop_m].vertex[0].y; // 第一點的 Y 分量

   z_m = sector1.triangle[loop_m].vertex[0].z; // 第一點的 Z 分量

   u_m = sector1.triangle[loop_m].vertex[0].u; // 第一點的 U  紋理坐标

   v_m = sector1.triangle[loop_m].vertex[0].v; // 第一點的 V  紋理坐标

   glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m); // 設定紋理坐标和頂點

   x_m = sector1.triangle[loop_m].vertex[1].x; // 第二點的 X 分量

   y_m = sector1.triangle[loop_m].vertex[1].y; // 第二點的 Y 分量

   z_m = sector1.triangle[loop_m].vertex[1].z; // 第二點的 Z 分量

   u_m = sector1.triangle[loop_m].vertex[1].u; // 第二點的 U  紋理坐标

   v_m = sector1.triangle[loop_m].vertex[1].v; // 第二點的 V  紋理坐标

   x_m = sector1.triangle[loop_m].vertex[2].x; // 第三點的 X 分量

   y_m = sector1.triangle[loop_m].vertex[2].y; // 第三點的 Y 分量

   z_m = sector1.triangle[loop_m].vertex[2].z; // 第三點的 Z 分量

   u_m = sector1.triangle[loop_m].vertex[2].u; // 第二點的 U  紋理坐标

   v_m = sector1.triangle[loop_m].vertex[2].v; // 第二點的 V  紋理坐标

  glEnd();      // 三角形繪制結束

 return TRUE;       // 傳回

原文及其個版本源代碼下載下傳:

<a href="http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=10">http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=10</a>

沒有整理與歸納的知識,一文不值!高度概括與梳理的知識,才是自己真正的知識與技能。 永遠不要讓自己的自由、好奇、充滿創造力的想法被現實的架構所束縛,讓創造力自由成長吧! 多花時間,關心他(她)人,正如别人所關心你的。理想的騰飛與實作,沒有别人的支援與幫助,是萬萬不能的。

    本文轉自wenglabs部落格園部落格,原文連結:http://www.cnblogs.com/arxive/p/6239005.html,如需轉載請自行聯系原作者