第九課
     
  歡迎進入第九課. 現在的你應該對於 OpenGL 有相當的了解. 你已經學過設定 OpenGL 視窗的一切東西, 以及材質貼圖在一個旋轉的物件, 並且用到光源和混色. 這是第一個中高階的課程. 你將學習到下列的東西: 在 3D 畫面上移動點陣圖, 移除點陣圖中黑色的像素點 (使用混色技巧), 在黑白色的貼圖材質中加入彩色, 以及最後你將學習到如何去建立豐富色彩, 以及利用不同色彩貼圖混合的簡單動畫.

我們將修改第一課的程式碼來給本課程使用. 我們將在程式一開始加入一些新的變數. 我將重寫這一段的程式碼, 所以這將可以很容易的看出那兒作了修改.
 
     
#include <windows.h>						// Header File For Windows
#include <stdio.h>						// Header File For Standard Input/Output
#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

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 Fullscreen Mode By Default
     
  下列這幾行是新的. twinkletp 是布林變數, 表示它們只能設為 TRUE 或 FALSE. twinkle 會持續追蹤是否 twinkle 效果被啟動. tp 用來檢查是否 'T' 按鍵被按下或放開. (按下時 tp=TRUE, 放開時 tp=FALSE).  
     
BOOL	twinkle;						// Twinkling Stars
BOOL	tp;							// 'T' Key Pressed?
     
  num 會持續追蹤我們在畫面上畫出星星的數量. 它被設定為固定數. 這表示它不會在程式中被修改. 我們把它們定義為定數的理由就是因為你不可以重新定義一個陣列. 所以我們設定一個可容納 50 個星星的陣列, 在我們決定把 num 增加為 51 時, 陣列不可以加為 51, 所以錯誤就會發生. 你可以隨時改變這個值, 不過只能在這一行做改變. 不要在之後的程式碼試著改變 num 的值, 除非你想發生災難.  
     
const	num=50;							// Number Of Stars To Draw
     
  現在我們建立一個結構. 結構這個字眼聽起來很恐怖, 但是並不是如此. 一個結構是一群簡單的資料 (變數等等) 代表出一個較大的群組類別. 這是用國語來說的啦 :) 我們知道我們要持續追蹤星星. 你會看到下面第七行就是 stars; 我們知道每個星星有三個色彩值, 都是整數值. 第三行 int r,g,b 會設定三個整數值. 一個紅色 (r), 一個綠色 (g), 以及一個藍色 (b). 我們知道每個星星離畫面中心會有不同的距離, 而且被放置在畫面中 360 個角度中的其中之一. 如果你看到下列第四行, 我們用一個叫做 dist 的浮點數值. 它會持續記錄著距離值. 第五行用一個叫做 angle 的浮點數值. 用來持續記錄著星星的角度值.

所以我們就有一群資料描述著畫面上星星的色彩, 距離, 和角度. 不幸的是我們有一個以上的星星要持續追蹤. 我們只要建一個叫做 star 的陣列, 而不必產生 50 個紅色值, 50 個綠色值, 50 個藍色值, 50 個距離值, 以及 50 個角度值. star 的陣列會存放所有的資料在我們所稱做 stars 的結構中. 我們在下列第八行建一個 star 的陣列. 我們看一下第八行: stars star[num]. 陣列的類型是 stars. stars 是一個結構. 所以陣列會存放所有結構的資訊. 陣列的名字叫做 star. 陣列的大小是 [num]. 所以因為 num=50, 我們現在就有一個名為 star 的陣列. 我們的陣列存放 stars 結構的元素. 追蹤結構元素這樣會比各自分開的變數容易的多. 不過這樣也有點笨啦, 因為我們不能藉由改變定數 num 來增減星星的數量.
 
     
typedef struct							// Create A Structure For Star
{
	int r, g, b;						// Stars Color
	GLfloat dist;						// Stars Distance From Center
	GLfloat angle;						// Stars Current Angle
}
stars;								// Structures Name Is Stars
stars star[num];						// Make 'star' Array Of 'num' Using Info From The Structure 'stars'
     
  接下來我們設定變數來持續追蹤星星和觀察者之間的距離 (zoom), 而星星的傾斜角度 (tilt). 我們建一個叫做 spin 的變數, 會繞著 z 軸旋轉閃爍星星, 使它們看起來就像在目前的位置上旋轉一般.

loop 是一個變數, 我們將用在程式中畫出全部 50 個星星, 而 texture[1] 會用來存放一張我們要載入的黑白色貼圖材質. 如果你想要更多貼圖材質, 就把數值 1 改為你要使用的貼圖數量值即可.
 
     
GLfloat	zoom=-15.0f;						// Viewing Distance Away From Stars
GLfloat tilt=90.0f;						// Tilt The View
GLfloat	spin;							// Spin Twinkling Stars

GLuint	loop;							// General Loop Variable
GLuint	texture[1];						// Storage For One Texture

LRESULT	CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);		// Declaration For WndProc
     
  接著這幾行我們會加入程式碼來載入我們的材質. 我應該不需要詳細解釋這個程式碼了. 它和我們在第六, 七, 八課中用來載入貼圖的程式碼相同的. 我們這次載入的點陣圖叫做 star.bmp. 我們只用 glGenTextures(1, &texture[0]) 產生一個貼圖材質. 這個貼圖材質會使用線性濾鏡.  
     
AUX_RGBImageRec *LoadBMP(char *Filename)			// Loads A Bitmap Image
{
	FILE *File=NULL;					// File Handle

	if (!Filename)						// Make Sure A Filename Was Given
	{
		return NULL;					// If Not Return NULL
	}

	File=fopen(Filename,"r");				// Check To See If The File Exists

	if (File)						// Does The File Exist?
	{
		fclose(File);					// Close The Handle
		return auxDIBImageLoad(Filename);		// Load The Bitmap And Return A Pointer
	}
	return NULL;						// If Load Failed Return NULL
}
     
  這一段程式碼用來載入點陣圖 (藉由呼叫上一段程式碼), 並且將它轉換為貼圖材質. Status 是用來持續追蹤是否材質被載入或是被建立.  
     
int LoadGLTextures()						// Load Bitmaps And Convert To Textures
{
	int Status=FALSE;					// Status Indicator

	AUX_RGBImageRec *TextureImage[1];			// Create Storage Space For The Texture

	memset(TextureImage,0,sizeof(void *)*1);		// Set The Pointer To NULL

	// Load The Bitmap, Check For Errors, If Bitmap's Not Found Quit
	if (TextureImage[0]=LoadBMP("Data/Star.bmp"))
	{
		Status=TRUE;					// Set The Status To TRUE

		glGenTextures(1, &texture[0]);			// Create One Texture

		// Create Linear Filtered Texture
		glBindTexture(GL_TEXTURE_2D, texture[0]);
		glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
		glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
	}

	if (TextureImage[0])					// If Texture Exists
	{
		if (TextureImage[0]->data)			// If Texture Image Exists
		{
			free(TextureImage[0]->data);		// Free The Texture Image Memory
		}

		free(TextureImage[0]);				// Free The Image Structure
	}

	return Status;						// Return The Status
}
     
  現在我們設定 OpenGL 來畫出我們所要的. 在這個專案中我們將不會使用深度測試, 所以如果你使用第一課的程式碼時, 請確定 glDepthFunc(GL_LEQUAL); 和 glEnable(GL_DEPTH_TEST); 有被移除, 否則你將會看到一些很糟的結果. 我們在程式碼中使用材質貼圖, 所以你將會想要確定哪些新加入的程式碼是第一課中所沒有的. 你將會注意到我們正開啟了材質貼圖, 透過混色.  
     
int InitGL(GLvoid)						// All Setup For OpenGL Goes Here
{
	if (!LoadGLTextures())					// Jump To Texture Loading Routine
	{
		return FALSE;					// If Texture Didn't Load Return FALSE
	}

	glEnable(GL_TEXTURE_2D);				// Enable Texture Mapping
	glShadeModel(GL_SMOOTH);				// Enable Smooth Shading
	glClearColor(0.0f, 0.0f, 0.0f, 0.5f);			// Black Background
	glClearDepth(1.0f);					// Depth Buffer Setup
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);	// Really Nice Perspective Calculations
	glBlendFunc(GL_SRC_ALPHA,GL_ONE);			// Set The Blending Function For Translucency
	glEnable(GL_BLEND);					// Enable Blending
     
  下列的程式碼是新的. 它設定了每個星星的起始角度, 距離, 以及色彩. 注意到修改結構中的資訊是很重要的. 這個迴圈將會完成全部 50 個星星. 要修改 star[1] 的角度時, 我們所要做的只有 star[1].angle={某一個值}. 夠簡單了吧!  
     
	for (loop=0; loop<num; loop++)				// Create A Loop That Goes Through All The Stars
	{
		star[loop].angle=0.0f;				// Start All The Stars At Angle Zero
     
  我計算出目前星星的距離 (使用 loop 的值), 用它來除以全部星星的數量值. 接著再把結果乘上 5.0f. 基本上這會讓每個星星比前一個星星要遠一些. 當 loop 是 50 (最後一個星星), loop 除以 num 會等於 1.0f. 我把它在乘上 5.0f 的理由是因為 1.0f*5.0f 是 5.0f. 5.0f 很接近畫面邊緣. 我並不想讓星星畫到畫面之外, 所以 5.0f 是個恰恰好的值. 如果你把 zoom 設定為更深入畫面的話, 那你就可以用比 5.0f 更大的值, 但是你的星星看起來就會比較小 (因為透視觀點的關係).

你將會注意到每個星星的顏色是用介於 0 到 255 的亂數值. 你或許會懷疑為何我們可以設定如此大的值呢, 一般的色彩值應該介於 0.0f 到 1.0f 之間, 不是嗎? 當我們設定色彩時, 我們將使用 glColor4ub 而不是 glColor4f. ub 表示無號位元. 一個位元的值可以是 0 到 255 裡的值. 在這個程式中, 使用位元會比使用亂數的浮點值來的容易些.
 
     
		star[loop].dist=(float(loop)/num)*5.0f;		// Calculate Distance From The Center
		star[loop].r=rand()%256;			// Give star[loop] A Random Red Intensity
		star[loop].g=rand()%256;			// Give star[loop] A Random Green Intensity
		star[loop].b=rand()%256;			// Give star[loop] A Random Blue Intensity
	}
	return TRUE;						// Initialization Went OK
}
     
  這個更改大小的程式碼是相同的, 所以我們將進入繪圖程式碼. 如果你使用第一課的程式碼, 那就刪掉 DrawGLScene 程式碼, 只要把下列的程式碼複製過來就好了. 第一課的這一段也只有兩行程式碼罷了, 所以並沒有一堆要刪除的東東.  
     
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
	glBindTexture(GL_TEXTURE_2D, texture[0]);		// Select Our Texture

	for (loop=0; loop<num; loop++)				// Loop Through All The Stars
	{
		glLoadIdentity();				// Reset The View Before We Draw Each Star
		glTranslatef(0.0f,0.0f,zoom);			// Zoom Into The Screen (Using The Value In 'zoom')
		glRotatef(tilt,1.0f,0.0f,0.0f);			// Tilt The View (Using The Value In 'tilt')
     
  現在我們移動星星. 星星一開始會在畫面的中間. 我們所要做的第一件事就是將畫面繞著 y 軸旋轉. 如果我們旋轉了 90 度, 那 x 軸就不再是左右向了, 而會變成垂直畫面的深淺了. 舉一個清楚的例子. 想像你身在一個房間的中央. 左邊的牆壁上寫了 -x, 前面的牆壁上寫了 -z, 右邊的牆壁上寫了 +x, 後面的牆壁上寫了 +z. 如果房間向右旋轉 90 度, 而你不動, 那麼前面的牆壁就不會是 -z, 而會是 -x. 所有的牆壁都會移動. 之前的 -z 在前面, 而 +z 在後面, 所以現在 -x 在前面而 +x 在後面. 懂了嗎? 旋轉場景後, 我們會改變 x 和 z 平面的方向.

第二行程式碼在 x 平面移動一個正值. 通常一個 x 上的正值會把我們移往畫面的右邊 (通常會是 +x), 但是因為我們旋轉了 y 軸, +x 就不會左右移了. 如果我們繞 y 軸旋轉 180 度, 那麼畫面的左右就會互換了. 所以我們在移往正值的 x 軸時, 就可以在畫面上的前後左右移動了.
 
     
		glRotatef(star[loop].angle,0.0f,1.0f,0.0f);	// Rotate To The Current Stars Angle
		glTranslatef(star[loop].dist,0.0f,0.0f);	// Move Forward On The X Plane
     
  現在有一些技巧性的程式碼. 星星事實上是平面貼圖. 現在如果你在畫面中央畫一個平面四方形, 並且將它貼上材質貼圖, 看起來就會很好. 它會依照你所期望的面向著你. 但是如果你繞 y 軸旋轉 90 度, 貼圖就會朝向畫面的左右邊. 而你就只會看到一條細線. 我們並不希望這樣的事發生. 我們想要的是星星可以一直面向畫面, 不論我們如何旋轉以及傾斜畫面.

我們只要再畫星星之前取消任何的旋轉, 就可以達成它, 你以相反的順序取消旋轉. 所以以上我們傾斜畫面, 然後把星星旋轉到目前的角度. 在相反的順序下, 我們逆旋轉星星目前的角度. 為了這麼做, 我們使用負值的角度, 並以此旋轉. 所以如果星星旋轉 10 度, 那就轉回 -10 度, 星星就會在該軸上轉回面向畫面的角度. 因此下列第一行取消了 y 軸的旋轉. 接著我們需要取消畫面 x 軸的傾斜. 要達成這個, 只要把畫面傾斜 -tilt 值即可. 隨後我們就取消了 x 和 y 軸的旋轉, 那麼星星就完全的面向畫面了.
 
     
		glRotatef(-star[loop].angle,0.0f,1.0f,0.0f);	// Cancel The Current Stars Angle
		glRotatef(-tilt,1.0f,0.0f,0.0f);		// Cancel The Screen Tilt
     
  如果 twinkle 是 TRUE, 我們就畫出一個沒有旋轉的星星在畫面上. 要用別的色彩, 我們把星星數的最大值 (num) 減去目前的星星數量 (loop), 再減去 1, 因為 loop 值是由 0 到 num-1. 如果結果是 10, 那我們就把這個色彩設定給編號 10 的星星. 這樣一來兩個星星的顏色通常會是不同的. 這不是個好方法, 不過是很有效的. 最後的值是透明度值. 值越低的話, 那星星就會更暗.

如果 twinkle 被啟動, 那每個星星就會被畫兩次. 這會使程式執行的速度較慢些, 看你是使用哪種等級的電腦. 如果 twinkle 被啟動, 兩個星星的顏色就會被混合成真正完美的色彩. 也因為第二次畫的星星不會旋轉, 所以當 twinkle 被啟動時, 星星看起來就像是動畫般的. (你可以自己看看如果你不懂我說的意思)

注意到要在貼圖中加入色彩是很容易的. 甚至貼圖是黑白, 那它就會依照我們在畫貼圖前的所指定的色彩來畫這個貼圖材質. 也請你注意到, 我們用位元來表示色彩值, 而不是用浮點數. 甚至透明度值也是一個位元.
 
     
		if (twinkle)					// Twinkling Stars Enabled
		{
			// Assign A Color Using Bytes
			glColor4ub(star[(num-loop)-1].r,star[(num-loop)-1].g,star[(num-loop)-1].b,255);
			glBegin(GL_QUADS);			// Begin Drawing The Textured Quad
				glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f,-1.0f, 0.0f);
				glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f,-1.0f, 0.0f);
				glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 0.0f);
				glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 0.0f);
			glEnd();				// Done Drawing The Textured Quad
		}
     
  現在我們畫出主要的星星. 這和上一段程式碼唯一的不同就是這個星星會一直被畫出來, 而且這個星星會繞著 z 軸旋轉.  
     
		glRotatef(spin,0.0f,0.0f,1.0f);			// Rotate The Star On The Z Axis
		// Assign A Color Using Bytes
		glColor4ub(star[loop].r,star[loop].g,star[loop].b,255);
		glBegin(GL_QUADS);				// Begin Drawing The Textured Quad
			glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f,-1.0f, 0.0f);
			glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f,-1.0f, 0.0f);
			glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 0.0f);
			glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 0.0f);
		glEnd();					// Done Drawing The Textured Quad
     
  這裡是我們處理所有移動的地方. 我們透過增加 spin 的值來旋轉一般的星星. 然後我們改變每個星星的角度. 每個星星的角度依照 loop/num 來增加. 這會讓越是遠離中心點的星星旋轉的越快. 接近中心點的星星就旋轉的比較慢. 最後我們減少每個星星距離畫面中心的值. 這會讓星星看起來像是被吸入畫面中心一般.  
     
		spin+=0.01f;					// Used To Spin The Stars
		star[loop].angle+=float(loop)/num;		// Changes The Angle Of A Star
		star[loop].dist-=0.01f;				// Changes The Distance Of A Star
     
  下面這幾行用來檢查星星是否已經撞到畫面的中心了. 當一個星星撞擊到畫面的中心時, 那就給它一個新的色彩, 並且把它移到離中心 5 個單位的位置, 所以它就開始它的新旅程, 重新的回到中心點像一個新的星星.  
     
		if (star[loop].dist<0.0f)			// Is The Star In The Middle Yet
		{
			star[loop].dist+=5.0f;			// Move The Star 5 Units From The Center
			star[loop].r=rand()%256;		// Give It A New Red Value
			star[loop].g=rand()%256;		// Give It A New Green Value
			star[loop].b=rand()%256;		// Give It A New Blue Value
		}
	}
	return TRUE;						// Everything Went OK
}
     
  現在我們將加入程式碼來檢查是否有按鍵被按下. 在 WinMain() 下. 找到這一行 SwapBuffers(hDC). 我們將加入我們的按鍵檢查碼在適當的程式碼位置.

這行下檢查看看是否 T 按鍵被按下. 如果它被按下而且不是一直被按著的, 那下列的是就會發生. 如果 twinkle 是 FALSE, 它就會變為 TRUE. 如果它是 TRUE, 它就會變成 FALSE. 一但 T 被按下, tp 就會變成 TRUE. 這會避免程式碼一直反覆執行在你一直按下 T 按鍵時.
 
     
		SwapBuffers(hDC);				// Swap Buffers (Double Buffering)
		if (keys['T'] && !tp)				// Is T Being Pressed And Is tp FALSE
		{
			tp=TRUE;				// If So, Make tp TRUE
			twinkle=!twinkle;			// Make twinkle Equal The Opposite Of What It Is
		}
     
  這個程式碼會檢查看看是否你放開了 T 按鍵. 如果你放開了, 那 tp=FALSE. 按下 T 按鍵時什麼都不會發生除非 tp 是 FALSE, 所以這一段程式碼是很重要的.  
     
		if (!keys['T'])					// Has The T Key Been Released
		{
			tp=FALSE;				// If So, make tp FALSE
		}
     
  接下來的程式碼用來檢查看看是否上下方向鍵, 上一頁下一頁的按鍵被按下.  
     
		if (keys[VK_UP])				// Is Up Arrow Being Pressed
		{
			tilt-=0.5f;				// Tilt The Screen Up
		}

		if (keys[VK_DOWN])				// Is Down Arrow Being Pressed
		{
			tilt+=0.5f;				// Tilt The Screen Down
		}

		if (keys[VK_PRIOR])				// Is Page Up Being Pressed
		{
			zoom-=0.2f;				// Zoom Out
		}

		if (keys[VK_NEXT])				// Is Page Down Being Pressed
		{
			zoom+=0.2f;				// Zoom In
		}
     
  就像所有之前的課程, 請確定視窗上的標題列是正確的.  
     
		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's Textures, Lighting & Keyboard Tutorial",640,480,16,fullscreen))
			{
				return 0;			// Quit If Window Was Not Created
			}
		}
	}
}
     
  在這課程中, 我已經試著詳細的解釋如何載入一個灰階的點陣圖, 移除圖形中黑色的部分 (使用混色), 在圖形中加入色彩, 並且在 3D 下移動螢幕畫面中的圖像. 我也秀出如何建立漂亮的色彩以及動畫, 藉由重疊第二份點陣圖的拷貝在原本的點陣圖上. 一但你好好的了解了我到目前為止所教你的東西, 你應該就能夠創造出你自己的 3D 展示程式. 所有的基本知識都已經包含在其中了!

Jeff Molofee (NeHe)

* 下載 Visual C++ 程式碼給本課程的.
* 下載 Visual Fortran 程式碼給本課程的. ( Conversion by Jean-Philippe Perois )
* 下載 Delphi 程式碼給本課程的. ( Conversion by Marc Aarts )
* 下載 Linux 程式碼給本課程的. ( Conversion by Richard Campbell )
* 下載 Irix 程式碼給本課程的. ( Conversion by Lakmal Gunasekara )
* 下載 Solaris 程式碼給本課程的. ( Conversion by Lakmal Gunasekara )
* 下載 Mac OS 程式碼給本課程的. ( Conversion by Anthony Parker )
* 下載 Power Basic 程式碼給本課程的. ( Conversion by Angus Law )
* 下載 BeOS 程式碼給本課程的. ( Conversion by Chris Herborth )
* 下載 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 翻譯