第十課
     
  這個課程是由 Lionel Brits (絽telgeuse) 所建立的. 這一課只會解釋新加入的程式區段. 不過只加入下列的程式碼時, 程式並不能執行的. 如果你有興趣想知道下列每一行程式碼如何執行的話, 就去下載原始程式碼, 並且一步步的追蹤它, 在你閱讀這一課時.

歡迎來到沒名氣的第十課. 到現在為止, 你已經可以旋轉立方體或一對聯合的星星, 因此你對於 3D 程式設計已有基本的感覺了. 但是等一下! 還不要馬上就衝動的要開始寫 Quake IV 的程式碼. 旋轉的立方體還沒辦法創造出很酷的死戰對手的 :-) 這些天你會需要一個很大, 複雜的並且動態的 3D 世界, 其中包含了六個自由度以及花俏的特效如鏡子, 入口, 變形以及理所當然要有快速的畫面更新率. 這一課解釋了 3D 世界的基本 "結構", 並且如何在其中移動著.

資料結構

當你想要設計出完美程式碼, 用一系列的數值來的建構一個 3D 環境時, 那當環境的複雜度增加時, 這會變的越來越困難. 舉例來說, 我們必須對於我們的資料加以分門別類, 成為可以使用的樣式. 在我們列表的開頭就是在這一段中. 每個 3D 世界基本上就是這一區段的集合. 一個區段可以是一間房間, 一個立方體, 或是任何的封閉區間.
 
     
typedef struct tagSECTOR						// Build Our Sector Structure
{
	int numtriangles;						// Number Of Triangles In Sector
	TRIANGLE* triangle;						// Pointer To Array Of Triangles
} SECTOR;								// Call It SECTOR
     
  一個區段存放著一系列的多邊形, 所以下一個類別就是三角形 (我們只採用三角形, 這樣在設計程式碼上會比較容易.)  
     
typedef struct tagTRIANGLE						// Build Our Triangle Structure
{
	VERTEX vertex[3];						// Array Of Three Vertices
} TRIANGLE;								// Call It TRIANGLE
     
  三角形是最基本的多邊形, 由頂點所組成 (複數以上的頂點), 它帶給我們最後的類別. 頂點所存放的資料才是 OpenGL 所最感興趣的. 我們定義三角形中每個點在 3D 空間 (x, y, z) 中的位置, 以及它的貼圖座標 (u, v).  
     
typedef struct tagVERTEX						// Build Our Vertex Structure
{
	float x, y, z;							// 3D Coordinates
	float u, v;							// Texture Coordinates
} VERTEX;								// Call It VERTEX
     
  Loading files

在我們的程式中存放我們的世界座標資料, 會讓我們的程式變很固定而且無聊. 然而, 由磁碟中載入世界資料會讓我們有更多的彈性, 可以測試不同的世界而不必重新編譯我們的程式碼. 另一個優點就是使用者可以互換資料以及修改它們, 但是並不需要知道在我們的程式中是如何處理這些進進出出的資料. 我們將用文字格式作為資料檔案的型態. 這樣比較容易編輯, 並且只需要較少的程式碼. 我們將保留二進位的檔案格式在以後的運作上.

問題來了, 我們將如何有檔案中拿到資料呢. 首先我們建立一個新的函式叫做是 SetupWorld(). 我們把檔案定義為 filein, 並以唯讀存取的方式開啟它. 我們也必須在使用完後關閉這個檔案. 讓我們看看目前的程式碼:
 
     
// Previous Declaration: char* worldfile = "data\\world.txt";
void SetupWorld()							// Setup Our World
{
	FILE *filein;							// File To Work With
	filein = fopen(worldfile, "rt");				// Open Our File

	...
	(read our data)
	...

	fclose(filein);							// Close Our File
	return;								// Jump Back
}
     
  我們的下一個挑戰就是讀取每一行的文字到變數中. 這有一堆的方式可以達成. 我們的問題是: 並不是檔案中所有的資料行所存放的資料都是有意義的. 空白行和註解不需要被讀取. 讓我們建立一個叫做 readstr() 的函式. 這個函式將會讀取有意義的文字行, 把它轉為初始字串. 以下這就是程式碼:  
     
void readstr(FILE *f,char *string)					// Read In A String

{
	do	return;							// Start A Loop
	{
		fgets(string, 255, f);					// Read One Line
	} while ((string[0] == '/') || (string[0] == '\n'));		// See If It Is Worthy Of Processing
	return;								// Jump Back
}
     
  下一步, 我們必須讀入這一段的資料. 這個課程只處理一個區段, 不過要實作出多區段的引擎也是很簡單的. 讓我們回到 SetupWorld(). 我們的程式必須知道我們的段落中有多少個三角形. 在我們的資料檔中, 我們將會定義三角形的數量如下:
NUMPOLLIES n
這兒的程式碼就是讀取出三角形數量之用:
 
     
int numtriangles;							// Number Of Triangles In Sector
char oneline[255];							// String To Store Data In
...
readstr(filein,oneline);						// Get Single Line Of Data
sscanf(oneline, "NUMPOLLIES %d\n", &numtriangles);			// Read In Number Of Triangles
     
  我們世界載入程序將會使用相同的程序. 接著, 我們將初始化我們的區段, 並把一些資料讀入其中:  
     
// Previous Declaration: SECTOR sector1;
char oneline[255];							// String To Store Data In
int numtriangles;							// Number Of Triangles In Sector
float x, y, z, u, v;							// 3D And Texture Coordinates
...
sector1.triangle = new TRIANGLE[numtriangles];				// Allocate Memory For numtriangles And Set Pointer
sector1.numtriangles = triangles;					// Define The Number Of Triangles In Sector 1
// Step Through Each Triangle In Sector
for (int triloop = 0; triloop < numtriangles; triloop++)		// Loop Through All The Triangles
{
	// Step Through Each Vertex In Triangle
	for (int vertloop = 0; vertloop < 3; vertloop++)		// Loop Through All The Vertices
	{
		readstr(filein,oneline);				// Read String To Work With
		// Read Data Into Respective Vertex Values
		sscanf(oneline, "%f %f %f %f %f %f %f", &x, &y, &z, &u, &v);
		// Store Values Into Respective Vertices
		sector1.triangle[triloop].vertex[vertloop].x = x;	// Sector 1, Triangle triloop, Vertice vertloop, x Value=x
		sector1.triangle[triloop].vertex[vertloop].y = y;	// Sector 1, Triangle triloop, Vertice vertloop, y Value=y
		sector1.triangle[triloop].vertex[vertloop].z = z;	// Sector 1, Triangle triloop, Vertice vertloop, z Value=z
		sector1.triangle[triloop].vertex[vertloop].u = u;	// Sector 1, Triangle triloop, Vertice vertloop, u Value=u
		sector1.triangle[triloop].vertex[vertloop].v = v;	// Sector 1, Triangle triloop, Vertice vertloop, v Value=v
	}
}
     
  每一個三角形在我們的資料檔中被宣告如下:
X1 Y1 Z1 U1 V1
X2 Y2 Z2 U2 V2
X3 Y3 Z3 U3 V3
顯示世界

現在我們可以把區段載入記憶體中, 我們必須把它顯示在螢幕中. 只要我們做好一些小的旋轉和轉移, 但是我們的攝影機都會在中心原點 (0,0,0) 上. 任何好的 3D 引擎都可以讓使用者走動並探索這個世界, 我們的也將會是. 要這麼做的一個方法就是四處移動攝影機, 並且畫出 3D 環境與攝影機的相對位置. 這會很慢並且很難寫出程式碼. 我們將會這麼做:

  1. 根據使用者的命令旋轉並轉移攝影機位置
  2. 以與攝影機相反的方向, 繞著原點旋轉世界 (這會給人感覺是攝影機在被旋轉的幻覺)
  3. 以與攝影機相反的方式, 轉移世界座標 (再一次的, 這會給人感覺是攝影機在被移動的幻覺)
這很容易實作出來. 讓我們開始第一個階段 (攝影機的旋轉以及轉移).
 
     
if (keys[VK_RIGHT])							// Is The Right Arrow Being Pressed?
{
	yrot -= 1.5f;							// Rotate The Scene To The Left
}


if (keys[VK_LEFT]) // Is The Left Arrow Being Pressed? { yrot += 1.5f; // Rotate The Scene To The Right }

if (keys[VK_UP]) // Is The Up Arrow Being Pressed? { xpos -= (float)sin(yrot*piover180) * 0.05f; // Move On The X-Plane Based On Player Direction zpos -= (float)cos(yrot*piover180) * 0.05f; // Move On The Z-Plane Based On Player Direction if (walkbiasangle >= 359.0f) // Is walkbiasangle>=359? { walkbiasangle = 0.0f; // Make walkbiasangle Equal 0 } else // Otherwise { walkbiasangle+= 10; // If walkbiasangle < 359 Increase It By 10 } walkbias = (float)sin(walkbiasangle * piover180)/20.0f; // Causes The Player To Bounce }

if (keys[VK_DOWN]) // Is The Down Arrow Being Pressed? { xpos += (float)sin(yrot*piover180) * 0.05f; // Move On The X-Plane Based On Player Direction zpos += (float)cos(yrot*piover180) * 0.05f; // Move On The Z-Plane Based On Player Direction if (walkbiasangle <= 1.0f) // Is walkbiasangle<=1? { walkbiasangle = 359.0f; // Make walkbiasangle Equal 359 } else // Otherwise { walkbiasangle-= 10; // If walkbiasangle > 1 Decrease It By 10 } walkbias = (float)sin(walkbiasangle * piover180)/20.0f; // Causes The Player To Bounce }
     
  那相當的簡單. 當向左或向右鍵被按下時, 旋轉變數 yrot 就會適當的被遞增或遞減. 當向前或向後鍵被按下時, 一個新的攝影機位置就會依照 三角函數 sine 和 cosine 來被計算出來 (需要懂一些三角函數 :-).Piover180 是容易轉換角度和弧度的因子.

接著你會問我: 什麼是 walkbias? 那是我發明的一個字 :-) 它基本上是一個位移值, 它會產生再當一個人四處走時 (頭像浮標般的上下擺動). 它可以很簡單的使用 sine 正弦波來調整攝影機的 Y 座標值. 我必須把這個放進去, 不然只有簡單的前後移動看起來並不夠好.

現在我們必須有以下的變數, 那我們就可以進行第二以及第三個步驟. 這可以在顯示迴圈中被完成, 只要我們的程式不會複雜到需要切割出另一個函式的話.
 
     
int DrawGLScene(GLvoid)							// Draw The OpenGL Scene
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);		// Clear Screen And Depth Buffer
	glLoadIdentity();						// Reset The Current Matrix

	GLfloat x_m, y_m, z_m, u_m, v_m;				// Floating Point For Temp X, Y, Z, U And V Vertices
	GLfloat xtrans = -xpos;						// Used For Player Translation On The X Axis
	GLfloat ztrans = -zpos;						// Used For Player Translation On The Z Axis
	GLfloat ytrans = -walkbias-0.25f;				// Used For Bouncing Motion Up And Down
	GLfloat sceneroty = 360.0f - yrot;				// 360 Degree Angle For Player Direction

	int numtriangles;						// Integer To Hold The Number Of Triangles

	glRotatef(lookupdown,1.0f,0,0);					// Rotate Up And Down To Look Up And Down
	glRotatef(sceneroty,0,1.0f,0);					// Rotate Depending On Direction Player Is Facing
	
	glTranslatef(xtrans, ytrans, ztrans);				// Translate The Scene Based On Player Position
	glBindTexture(GL_TEXTURE_2D, texture[filter]);			// Select A Texture Based On filter
	
	numtriangles = sector1.numtriangles;				// Get The Number Of Triangles In Sector 1
	
	// Process Each Triangle
	for (int loop_m = 0; loop_m < numtriangles; loop_m++)		// Loop Through All The Triangles
	{
		glBegin(GL_TRIANGLES);					// Start Drawing Triangles
			glNormal3f( 0.0f, 0.0f, 1.0f);			// Normal Pointing Forward
			x_m = sector1.triangle[loop_m].vertex[0].x;	// X Vertex Of 1st Point
			y_m = sector1.triangle[loop_m].vertex[0].y;	// Y Vertex Of 1st Point
			z_m = sector1.triangle[loop_m].vertex[0].z;	// Z Vertex Of 1st Point
			u_m = sector1.triangle[loop_m].vertex[0].u;	// U Texture Coord Of 1st Point
			v_m = sector1.triangle[loop_m].vertex[0].v;	// V Texture Coord Of 1st Point
			glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);	// Set The TexCoord And Vertice
			
			x_m = sector1.triangle[loop_m].vertex[1].x;	// X Vertex Of 2nd Point
			y_m = sector1.triangle[loop_m].vertex[1].y;	// Y Vertex Of 2nd Point
			z_m = sector1.triangle[loop_m].vertex[1].z;	// Z Vertex Of 2nd Point
			u_m = sector1.triangle[loop_m].vertex[1].u;	// U Texture Coord Of 2nd Point
			v_m = sector1.triangle[loop_m].vertex[1].v;	// V Texture Coord Of 2nd Point
			glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);	// Set The TexCoord And Vertice
			
			x_m = sector1.triangle[loop_m].vertex[2].x;	// X Vertex Of 3rd Point
			y_m = sector1.triangle[loop_m].vertex[2].y;	// Y Vertex Of 3rd Point
			z_m = sector1.triangle[loop_m].vertex[2].z;	// Z Vertex Of 3rd Point
			u_m = sector1.triangle[loop_m].vertex[2].u;	// U Texture Coord Of 3rd Point
			v_m = sector1.triangle[loop_m].vertex[2].v;	// V Texture Coord Of 3rd Point
			glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);	// Set The TexCoord And Vertice
		glEnd();						// Done Drawing Triangles
	}
	return TRUE;							// Jump Back
}
     
  接著我們畫出我們的第一個畫面. 這不會像 Quake, 不過你也幫幫忙, 我們又不是像 Carmack 或 Abrash 所做的東東. 當程式在執行時, 你可以按下 F, B, PgUp 以及 PgDown 來看看所加入的效果. PgUp/Down 會簡明的上下傾斜攝影機 (就像把鏡頭由一邊帶到另一邊相同的程序.) 貼圖材質是一個單純的泥巴樣的材質, 並且以我的學校識別證上的照片來套用為凹凸面的貼圖; 如果 NeHe 決定保留它的話, 那就會是這樣的 :-).

所以現在你大概會想著要走去那兒. 不過可別想用這個程式碼來作一個完整的 3D 引擎, 因為這不是為了這個目的所設計的. 或許你想有一個以上的段落在你的遊戲中, 特別是如果你將要入門實作的話. 你也將會想用有三個頂點以上的多邊形, 再一次的, 對於基本的入門引擎. 我目前做出來的程式碼允許載入多個區段, 並且有作背面刪去法 (就是不畫出背面向著攝影機的多邊形). 我會儘快的寫出這個課程. 但是它會用到一大堆的數學, 我將會先寫一個關於矩陣的課程.

NeHe (05/01/00): 我已經加入了完整的註解在這個課程的每一行程式碼上. 希望一切會更容易懂些. 本來只有一部份的程式行上有註解的, 現在是全部都有了 :)

如果你對於這個程式碼/課程有任何的問題 (這是我的第一個課程, 所以我的解說有點模糊), 請別客氣的寄電子郵件告訴我 mailto:iam@cadvision.com

Lionel Brits (絽telgeuse)

* 下載 Visual C++ 程式碼給本課程的.
* 下載 Irix 程式碼給本課程的. ( Conversion by Rob Fletcher )
* 下載 Linux 程式碼給本課程的. ( Conversion by Richard Campbell )
* 下載 Visual Fortran 程式碼給本課程的. ( Conversion by Jean-Philippe Perois )
* 下載 Delphi 程式碼給本課程的. ( Conversion by Marc Aarts )
* 下載 Mac OS 程式碼給本課程的. ( Conversion by Anthony Parker )
* 下載 Power Basic 程式碼給本課程的. ( Conversion by Angus Law )
* 下載 Java 程式碼給本課程的. ( Conversion by Darren Hodges )
* 下載 MingW32 & Allegro 程式碼給本課程的. ( Conversion by Peter Puck )
* 下載 Borland C++ Builder 4.0 程式碼給本課程的. ( Conversion by Patrick Salmons )
 
     
 
Back To NeHe Productions!
回到 OpenGL 教學索引
中文版由 Macbear 翻譯