第一課
     
  歡迎進入我的 OpenGL 課程. 我是一個熱愛 OpenGL 的平凡人! 我第一次聽到 OpenGL 的時候是在 3Dfx 推出她的巫毒一代 (Voodoo 1) 硬體加速卡的 OpenGL 驅動程式. 很快的我知道我必須去學習 OpenGL. 不過很不幸的, 不論是透過書籍或者是網路, 關於 OpenGL 的資訊卻很難找得到. 我花了數小時去試著讓程式碼運作, 甚至花了更多時間透過電子郵件和聊天室 (IRC) 來請求別人的協助. 我發現這那些懂 OpenGL 的人都自認為自己是菁英份子, 卻沒有興趣來和別人分享他們的知識. 真是夠了!

因此我建立這個網站的目的就是讓想學 OpenGL 的人能有一個可以尋求協助的地方. 我會盡可能詳細的嘗試去解釋我的課程內容, 包括每一行程式在做什麼. 我也試著讓程式學起來更簡潔 (而不會去使用 MFC 程式碼)! 即使是對於 Visual C++ 和 OpenGL 的新手而言, 也應該可以看懂這些程式碼, 並且清楚的了解它. 我的網站只是眾多 OpenGL 教學網站之一. 如果你是一個 OpenGL 程式設計高手, 那我的網站對你而言或許會太簡單了. 不過如果你才剛開始學, 那麼我認為我的網站提供了很多東西!

這個課程在兩千年一月時候完全改寫過. 本課程會教你如何設定一個 OpenGL 視窗. 這個程式可以是任意大小的視窗模式或是任意解析度任意色彩數的全螢幕模式. 這個程式碼很有彈性, 可以使用在所有你的 OpenGL 專案中. 我所有的課程都架構在這個程式上! 我所寫的這個程式碼很有彈性也很實用. 所有的程式錯誤都有被找到. 所以應該不會有未釋放的記憶體問題, 這個程式碼相當容易讀, 也很容易修改. 特別感謝 Fredric Echols 對程式碼所做的修改!

我將開始進入這個課程的程式碼. 首先你要開始建立建立一個 Visual C++ 的專案. 如果你不知道如何建這個專案, 那你不應該現在學 OpenGL, 而應該先學 Visual C++. 被下載的程式碼用的是 Visual C++ 6.0. 有些版本的 Visual C++ 要將 bool 改成 BOOL, true 改成 TRUE, 以及 false 改成 FALSE. 經過這樣的修改後, 我已試過將程式碼用 Visual C++ 4.0 和 5.0 編譯過了, 是沒有問題的.

在你建立一個新的 Win32 應用程式 (不是一個 console 應用程式) 在 Visual C++ 上後, 你會需要連結 OpenGL 函式庫. 在 Visual C++ 的 Project, Setting 中, 點選 LINK 標籤. 在 "Object/Library Modules" 這一行的最前面 (在 kernel32.lib 前) 加上 OpenGL32.lib GLu32.libGLaux.lib. 接著按下 OK. 現在你已經準備好來寫一個 OpenGL 的視窗程式了.

開頭四行是各個函式庫的包含標頭檔. 看起來如下:
 
     
#include <windows.h>								// Header File For Windows
#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
     
  接著你需要設定在程式中所需使用的所有變數. 這個程式會建立一個空白的 OpenGL 視窗, 雖然我們還不需要設定一大堆變數. 我們所設定的一些變數是非常重要的, 並且會在你所寫的每一個 OpenGL 程式中使用到.

第一行是設定著色區. 每一個 OpenGL 程式都要連結到一個著色區. 著色區就是連結到 OpenGL 的裝置內容 (Device Context). OpenGL 的著色區定義為 hRC. 為了將你的程式畫到視窗上, 你必須建立一個裝置內容 (Device Context), 這就是第二行所做的. 視窗的裝置內容 (Device Context) 定義為 hDC. 這個 DC 連接到視窗的 GDI (圖形裝置介面). RC 連接 OpenGL 到 DC.

第三行變數 hWnd 存放視窗作業系統所分派的 handle. 最後, 第四行則是我們的程式所建立的 Instance.
 
     
HGLRC		hRC=NULL;							// Permanent Rendering Context
HDC		hDC=NULL;							// Private GDI Device Context
HWND		hWnd=NULL;							// Holds Our Window Handle
HINSTANCE		hInstance;							// Holds The Instance Of The Application
     
  下列第一行設定一個陣列用來監看鍵盤按鍵的狀態. 有許多方法可以檢查鍵盤按鍵的狀態, 不過我用的這個方法是很可靠的, 它可以處理數個按鍵同時被按下的狀況.

這個 active 變數是用來分辨我們的程式視窗是否被縮到最小化. 如果程式視窗縮到最小化時, 那我們就可以暫時中止程式運作. 我喜歡暫時中止程式, 這樣當它縮到最小化時就不會在背景繼續執行著.

變數 fullscreen 就相當清楚了. 如果我們的程式在全螢幕下執行, fullscreen 就會設為 TRUE; 如果在視窗模式下執行, fullscreen 則設為 FALSE. 將它設為全域變數是很重要的, 因為每個程序都必須知道到底程式是否在全螢幕下執行著.
 
     
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
     
  然後我們定義了 WndProc(). 因為 CreateGLWindow() 需要參照 WndProc(), 可是 WndProc() 寫在 CreateGLWindow() 之後. 在 C 語言中, 我們要將現在要用到的程序宣告在程式的開頭, 當這個程序在以後才會被寫到時. 因此這一行我們定義了 WndProc(), 那麼 CreateGLWindow() 就可以參考到 WndProc().  
     
LRESULT	CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);				// Declaration For WndProc
     
  下一段程式碼的工作就是變更 OpenGL 視窗的大小在程式視窗大小變動時 (假設是視窗模式下, 而非全螢幕模式). 甚至如果你不能改變執行視窗大小時 (例如: 在全螢幕模式下), 這個函式在程式一開始執行時, 至少會被執行一次, 以設定透視觀點. OpenGL 畫面會依據視窗顯示的寬高來調整大小.  
     
GLvoid ReSizeGLScene(GLsizei width, GLsizei height)				// Resize And Initialize The GL Window
{
	if (height==0)								// Prevent A Divide By Zero By
	{
		height=1;							// Making Height Equal One
	}

	glViewport(0, 0, width, height);					// Reset The Current Viewport
     
  接著這幾行用來設定畫面為透視觀點模式. 也就是指越遠的物件會越小. 這樣看起來會比較真實. 基於視窗的寬高, 用透視計算出 45 度的可視角度. 而 0.1f, 100.0f 則分別表示出我們所要畫在畫面上的起始和結束的深度.

glMatrixMode(GL_PROJECTION) 表示接下來的兩行程式碼會對投影矩陣發生作用. 而透視矩陣則用來在場景中加入透視. glLoadIdentity() 用來重新設定矩陣. 它將所選擇的矩陣設回原始狀態. 在 glLoadIdentity() 後就會設定場景為透視觀點. glMatrixMode(GL_MODELVIEW) 表示任何的轉變將會影響到模型觀點的矩陣. 模型觀點的矩陣就是我們物件資訊儲存的地方. 接著我們重新設定模型觀點的矩陣. 如果你不懂這是什麼東東, 別擔心. 我會在接下來的課程裡解釋的. 只要知道它做了什麼, 如果你想要一個良好的透視場景.
 
     
	glMatrixMode(GL_PROJECTION);						// Select The Projection Matrix
	glLoadIdentity();							// Reset The Projection Matrix

	// Calculate The Aspect Ratio Of The Window
	gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,0.1f,100.0f);

	glMatrixMode(GL_MODELVIEW);						// Select The Modelview Matrix
	glLoadIdentity();							// Reset The Modelview Matrix
}
     
  下面這一段程式碼就是用來設定 OpenGL. 我們設定了清除畫面的顏色, 我們開啟深度緩衝區, 以及平滑著色功能, 等等. 這個函式會在 OpenGL 視窗建立後才被呼叫. 這個程序會傳回一個值, 不過因為我們的初始化並沒有那麼複雜, 所以現在還不用管它.  
     
int InitGL(GLvoid)								// All Setup For OpenGL Goes Here
{
     
  接著這一行用來開啟平滑著色模式. 平滑著色會好好的混合多邊型上的顏色, 光滑的順著光線. 我會在別的教學中更仔細的解釋平滑著色.  
     
	glShadeModel(GL_SMOOTH);						// Enables Smooth Shading
     
  接著這一行設定清除畫面的顏色. 如果你不知道它怎麼做到的, 我很快的解釋一下. 顏色值的範圍由 0.0f 到 1.0f. 0.0f 表示最暗, 而 1.0f 表示最亮. glClearColor 第一個參數是紅色的強度, 第二個參數是綠色, 而第三個參數則是藍色. 最大值是 1.0f, 顏色就由這三原色的亮度來決定. 最後一個參數則是透明度. 在用來清除畫面時, 我們可以不用管這第四個參數. 現在把它設為 0.0f 就好了, 我會在別的教學中解釋它的用處.

你可以透過混合不同的三原色 (紅綠藍) 來產生出不同的顏色. 希望你在學校學過一些基礎. 所以如果你用 glClearColor(0.0f,0.0f,1.0f,0.0f), 那麼畫面就會清成藍色. 所以如果你用 glClearColor(0.5f,0.0f,0.0f,0.0f), 那麼畫面就會清成暗紅色, 不會比 (1.0f) 亮, 也不會比 (0.0f) 暗. 要產生白色的背景, 那就把所有的顏色都設成 (1.0f). 要產生黑色的背景, 那就把所有的顏色都設成 (0.0f).
 
     
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);					// Black Background
     
  之後這三行用來設定深度緩衝區. 深度緩衝區就像是畫面上的很多層級. 深度緩衝區記錄著物件在畫面上的深度. 我們在這個程式還不需要用到深度緩衝區, 只要知道每一個 OpenGL 程式在 畫面上繪製 3D 都會用到深度緩衝區. 它會排序出物件的順序, 所以當你畫的正方形在一個圓形的後面時, 這個方形就不會被畫到圓形前. 深度緩衝區是 OpenGL 很重要的一個部分.  
     
	glClearDepth(1.0f);							// Depth Buffer Setup
	glEnable(GL_DEPTH_TEST);						// Enables Depth Testing
	glDepthFunc(GL_LEQUAL);							// The Type Of Depth Test To Do
     
  接著我們要告訴 OpenGL 說我們要設定最佳的透視修正. 這會影響一點點的執行效能, 不過這樣的透視觀點看起來比較好.  
     
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);			// Really Nice Perspective Calculations
     
  最後傳回 TRUE. 如果想看看初始化是否成功, 我們可以檢查傳回值是 TRUE 或 FALSE. 你可以加上一些程式碼在有錯誤發生時傳回 FALSE. 現在我們不必管它.  
     
	return TRUE;								// Initialization Went OK
}
     
  這一段則是所有繪圖作業的地方. 任何你計畫顯示在畫面上的, 就在這段程式碼中. 如果你已經懂一點 OpenGL, 那你可以試著建立一些基本的圖形, 藉著加一些 OpenGL 程式碼在 glLoadIdentity() 後, return TRUE 前. 如果你是一個 OpenGL 新手, 等到下一個課程. 現在我們所做的就是將畫面清除成之前所決定的顏色, 清除深度緩衝區, 並且重設場景. 我們還不會畫出任何東西.

傳回 TRUE 是告知我們程式沒有任何問題. 如果你要程式因為某種原因而暫停, 只要在傳回 TRUE 之前先傳回 FALSE , 就可以告訴程式繪圖的程式碼失敗了. 程式就會退出.
 
     
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 Current Modelview Matrix
	return TRUE;								// Everything Went OK
}
     
  下一段程式碼則要在程式離開之前被呼叫的. KillGLWindow() 的工作就是釋放著色區, 裝置內容 (Device Context) 以及視窗的 Handle. 我已經加上許多錯誤檢查的處理. 如果程式沒有辦法刪除視窗的任一部份時, 一個含有錯誤訊息的訊息視窗就會彈出, 來告訴你出了什麼問題. 這麼一來就會很容易的找出程式出了什麼問題.  
     
GLvoid KillGLWindow(GLvoid)							// Properly Kill The Window
{
     
  在 KillGLWindow() 首先要做的就是檢查現在是不是在全螢幕模式下. 如果是的話, 那我們就要先切換回視窗桌面模式. 我們應該在取消全螢幕模式之前先毀滅視窗, 不過在某些顯示卡上, 如果在取消全螢幕模式之前先毀滅視窗, 那麼視窗的桌面就會被損壞. 因此我們會先取消全螢幕模式. 這樣可以避免視窗桌面被損壞, 就可以在 Nvidia 和 3dfx 的顯示卡上正常運作!  
     
	if (fullscreen)								// Are We In Fullscreen Mode?
	{
     
  我們用 ChangeDisplaySettings(NULL,0) 來讓電腦回到原本的視窗桌面. 第一個參數傳入 NULL, 而第二個參數傳入 0, 就會讓視窗作業系統回復到儲存在視窗註冊區 (包含預設的解析度, 色彩數, 以及螢幕顯示頻率等等...) 桌面的原始狀態. 然後切回桌面後, 要再將滑鼠游標顯示出來.  
     
		ChangeDisplaySettings(NULL,0);					// If So Switch Back To The Desktop
		ShowCursor(TRUE);						// Show Mouse Pointer
	}
     
  下面的程式碼用來檢查我們是否有著色區 (hRC). 如果沒有, 那程式就直接跳到下一段程式碼, 來檢查我們是否有裝置內容 (Device Context).  
     
	if (hRC)								// Do We Have A Rendering Context?
	{
     
  如果我們有一個著色區, 下面的程式碼會檢查我們能不能將它釋放掉 (將 hRC 分離自 hDC). 注意到我所用的錯誤檢查方法. 基本上我讓程式試著去釋放它 (用wglMakeCurrent(NULL,NULL)), 然後檢查它是不是成功的被釋放了. 這兒很技巧的將多行的程式碼寫成一行.  
     
		if (!wglMakeCurrent(NULL,NULL))					// Are We Able To Release The DC And RC Contexts?
		{
     
  如果我們沒有辦法釋放 DC 和 RC 的內容, MessageBox() 會彈出, 並且有一個錯誤訊息讓我們知道是 DC 或是 RC 沒被釋放掉. NULL 表示一個訊息方塊沒有父視窗. NULL 右邊的文字會顯示到訊息方塊中. "SHUTDOWN ERROR" 的文字則會顯示在訊息方塊上方的標題裡. 接著是 MB_OK, 表示訊息方塊上會有一個叫做 "OK" 的按鈕. MB_ICONINFORMATION 則在訊息方塊中畫出一個圓形小圖像, 裡面有一個小寫的 i.  
     
			MessageBox(NULL,"Release Of DC And RC Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
		}
     
  接著我們試著刪除著色區. 如果不成功, 就會彈出一個錯誤訊息.  
     
		if (!wglDeleteContext(hRC))					// Are We Able To Delete The RC?
		{
     
  如果我們沒辦法刪除著色區, 那下面的程式碼會彈出一個訊息方塊, 讓我們知道刪除 RC 沒有成功. hRC 會被設為 NULL.  
     
			MessageBox(NULL,"Release Rendering Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
		}
		hRC=NULL;							// Set RC To NULL
	}
     
  現在我們檢查程式有沒有一個裝置內容 (Device Context), 如果有, 就試著去釋放它. 如果不能釋放這個裝置內容 (Device Context), 就彈出一個錯誤訊息, 並且將 hDC 設為 NULL.  
     
	if (hDC && !ReleaseDC(hWnd,hDC))					// Are We Able To Release The DC
	{
		MessageBox(NULL,"Release Device Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
		hDC=NULL;							// Set DC To NULL
	}
     
  現在我們檢查是不是有視窗 Handle, 如果有, 就用 DestroyWindow(hWnd) 來消滅這個視窗程式. 如果我們不能消滅這個視窗程式, 那就彈出一個錯誤訊息, 並且將 hWnd 設為 NULL.  
     
	if (hWnd && !DestroyWindow(hWnd))					// Are We Able To Destroy The Window?
	{
		MessageBox(NULL,"Could Not Release hWnd.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
		hWnd=NULL;							// Set hWnd To NULL
	}
     
  最後該做的就是取消登記視窗的類別. 這讓我們能適當的殺掉這個視窗程式, 才不會在重新開啟別的視窗程式時出現這個錯誤訊息 "視窗類別已經被登記了".  
     
	if (!UnregisterClass("OpenGL",hInstance))			// Are We Able To Unregister Class
	{
		MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
		hInstance=NULL;									// Set hInstance To NULL
	}
}
     
  下一段程式碼就是建立我們的 OpenGL 視窗程式. 我嘗試許多方式去決定, 是否我應該建立一個不需要一堆額外程式碼的固定的全螢幕視窗, 或者是需要一堆額外程式碼而可以被修改為方便使用的視窗程式. 我決定用需要一堆額外程式碼方便使用的視窗程式, 這會是最好的選擇. 我常在電子郵件上被問到這樣的問題: 我要如何建立一個視窗程式, 而不是用全螢幕模式? 我要怎麼改變視窗的標題? 我要如何改變解析度或是視窗的圖素格式? 下面的程式碼就會有答案了! 因此這是比較好的學習材料, 會讓你的 OpenGL 程式寫起來更容易些!

就如你所見, 程序傳回布林值 BOOL (TRUE 或 FALSE), 它也需要五個參數: 視窗的標題 title, 視窗的寬度 width, 視窗的高度 height, 色彩位元數 bits (16/24/32), 以及全螢幕旗標 fullscreenflag TRUE 表示全螢幕模式, 或是 FALSE 表示視窗模式. 我們會傳回一個布林值來了解視窗程式是否被成功建立起來.
 
     
BOOL CreateGLWindow(char* title, int width, int height, int bits, bool fullscreenflag)
{
     
  當我們要求視窗作業系統為我們找一個適合的圖素格式時, 視窗作業系統最後找到的會被存放在這個變數 PixelFormat.  
     
	GLuint		PixelFormat;						// Holds The Results After Searching For A Match
     
  wc 用來放置視窗類別的結構. 視窗類別的結構存放有關視窗的資訊. 改變類別中不同的欄位會影響到視窗程式的外觀和行為. 每一個視窗程式都屬於一個視窗類別. 在你建立一個視窗程式之前, 你必須要為這個視窗註冊一個類別.  
     
	WNDCLASS	wc;							// Windows Class Structure
     
  dwExStyledwStyle 分別存放延伸的和一般的視窗形式的資訊. 我用變數來儲存這樣的形式, 因此我可以改變形式來決定我所要建立的視窗類型 (一個全螢幕模式用的彈出式視窗, 或是有邊框的視窗模式視窗).  
     
	DWORD		dwExStyle;						// Window Extended Style
	DWORD		dwStyle;						// Window Style
     
  接著這五行程式碼抓出長方形上左下右的值. 我們將使用這些值來調整我們的視窗, 所以我們所畫的區域就會是所想要的正確解析度. 一般如果我們建立一個 640x480 的視窗, 那麼視窗的邊界會佔掉一小部份的解析度.  
     
	RECT WindowRect;							// Grabs Rectangle Upper Left / Lower Right Values
	WindowRect.left=(long)0;						// Set Left Value To 0
	WindowRect.right=(long)width;						// Set Right Value To Requested Width
	WindowRect.top=(long)0;							// Set Top Value To 0
	WindowRect.bottom=(long)height;						// Set Bottom Value To Requested Height
     
  下一行程式碼我們將全域變數 fullscreen 等於 fullscreenflag. 所以如果我們建立全螢幕的視窗程式, 那麼變數 fullscreenflag 就會是 TRUE. 如果我們不讓變數 fullscreen 等於 fullscreenflag, 那變數 fullscreen 就還是 FALSE. 則當我們殺掉這個視窗時, 電腦是在全螢幕模式下, 不過變數 fullscreen 卻是 FALSE, 而不是 TRUE, 那電腦就沒辦法切回桌面. 天呀, 我希望這樣有意義. 基本上, 總歸而言, fullscreen 必須等於 fullscreenflag, 否則的話會有問題的.  
     
	fullscreen=fullscreenflag;						// Set The Global Fullscreen Flag
     
  下一段程式碼中, 我們抓到我們的視窗的 instance, 然後就定義我們的視窗類別.

CS_HREDRAW 和 CS_VREDRAW 形式是用來強迫視窗在改變大小時要重新繪製. CS_OWNDC 為視窗建立私有的 DC. 也就是指 DC 不能被別的應用程式所共用. WinProc 這個程序是用來監看我們的程式的訊息. 接著這兩個欄位設為零, 因為不需要額外的視窗資料. 接著設定 instance. 然後把 hIcon 設為 NULL, 表示我們的視窗程式不需要一個小圖示 ICON, 滑鼠指標則用標準的箭頭. 背景顏色可以不用管 (我們會在 OpenGL 中設定). 在視窗中我們不需要選單, 所以把它設為 NULL, 類別名稱可以自己隨便取, 我會簡明的用 "OpenGL".
 
     
	hInstance		= GetModuleHandle(NULL);			// Grab An Instance For Our Window
	wc.style		= CS_HREDRAW | CS_VREDRAW | CS_OWNDC;		// Redraw On Move, And Own DC For Window
	wc.lpfnWndProc		= (WNDPROC) WndProc;				// WndProc Handles Messages
	wc.cbClsExtra		= 0;						// No Extra Window Data
	wc.cbWndExtra		= 0;						// No Extra Window Data
	wc.hInstance		= hInstance;					// Set The Instance
	wc.hIcon		= LoadIcon(NULL, IDI_WINLOGO);			// Load The Default Icon
	wc.hCursor		= LoadCursor(NULL, IDC_ARROW);			// Load The Arrow Pointer
	wc.hbrBackground	= NULL;						// No Background Required For GL
	wc.lpszMenuName		= NULL;						// We Don't Want A Menu
	wc.lpszClassName	= "OpenGL";					// Set The Class Name
     
  現在我們註冊了類別. 如果有什麼錯誤, 就會彈出錯誤訊息. 按下錯誤訊息方塊的 OK 就會離開程式了.  
     
	if (!RegisterClass(&wc))						// Attempt To Register The Window Class
	{
		MessageBox(NULL,"Failed To Register The Window Class.","ERROR",MB_OK|MB_ICONEXCLAMATION);
		return FALSE;							// Exit And Return FALSE
	}
     
  現在我們檢查程式應該是在全螢幕模式下執行, 還是在視窗模式. 如果它應該在全螢幕模式下, 那我們就嘗試著設定為全螢幕模式.  
     
	if (fullscreen)								// Attempt Fullscreen Mode?
	{
     
  下一段程式碼似乎有人遇到許多的問題 ... 就是切入全螢幕模式. 在切入全螢幕模式時, 有一些很重要的東西你應該要記住. 那就是確定你在全螢幕模式所要用的寬和高, 要和所建立的視窗的寬高一樣; 最重要的是要在建立視窗前先設定為全螢幕模式. 在這個程式碼中, 你不必去擔心寬和高, 全螢幕和視窗模式的大小都會被設定為所想要的值.  
     
		DEVMODE dmScreenSettings;					// Device Mode
		memset(&dmScreenSettings,0,sizeof(dmScreenSettings));		// Makes Sure Memory's Cleared
		dmScreenSettings.dmSize=sizeof(dmScreenSettings);		// Size Of The Devmode Structure
		dmScreenSettings.dmPelsWidth	= width;			// Selected Screen Width
		dmScreenSettings.dmPelsHeight	= height;			// Selected Screen Height
		dmScreenSettings.dmBitsPerPel	= bits;				// Selected Bits Per Pixel
		dmScreenSettings.dmFields=DM_BITSPERPEL|DM_PELSWIDTH|DM_PELSHEIGHT;
     
  在上面的程式碼, 我們先清除了顯示的設定. 然後設定螢幕所要切換的寬度, 高度, 和色彩位元數. 接著的程式碼, 我們試著設定全螢幕模式所需的設定. 我們在 dmScreenSettings 存放關於寬度, 高度, 和色彩位元數的資訊. 下列這一行 ChangeDisplaySettings 試著將螢幕依照 dmScreenSettings 的數值切換. 我使用參數 CDS_FULLSCREEN 在切換模式時, 因為它應該要移掉畫面最下方的開始功能表, 另外, 當你切入或切出全螢幕模式時, 它也不會對桌面上的視窗移動或更改大小.  
     
		// Try To Set Selected Mode And Get Results.  NOTE: CDS_FULLSCREEN Gets Rid Of Start Bar.
		if (ChangeDisplaySettings(&dmScreenSettings,CDS_FULLSCREEN)!=DISP_CHANGE_SUCCESSFUL)
		{
     
  如果模式無法被設定, 那下面的程式碼就會被執行. 如果符合的全螢幕模式不存在, 那就會有訊息方塊彈出, 以提供兩種選擇... 在視窗下執行或是離開程式.  
     
			// If The Mode Fails, Offer Two Options.  Quit Or Run In A Window.
			if (MessageBox(NULL,"The Requested Fullscreen Mode Is Not Supported By\nYour Video Card. Use Windowed Mode Instead?","NeHe GL",MB_YESNO|MB_ICONEXCLAMATION)==IDYES)
			{
     
  如果使用者決定用視窗模式, 則變數 fullscreen 就變為 FALSE, 程式就繼續執行下去.  
     
				fullscreen=FALSE;				// Select Windowed Mode (Fullscreen=FALSE)
			}
			else
			{
     
  如果使用者決定離開, 會有一個訊息方塊彈出來告訴使用者程式將被關閉. 傳回 FALSE 讓我們的程式知道視窗並沒有成功的建立. 然後程式就離開了.  
     
				// Pop Up A Message Box Letting User Know The Program Is Closing.
				MessageBox(NULL,"Program Will Now Close.","ERROR",MB_OK|MB_ICONSTOP);
				return FALSE;					// Exit And Return FALSE
			}
		}
	}
     
  因為上面設定全螢幕的程式碼可能會失敗, 使用者有可能讓程式在視窗模式下執行, 所以在我們設定畫面的視窗類型之前, 我們要再檢查一下 fullscreen 是 TRUE 或 FALSE.  
     
	if (fullscreen)								// Are We Still In Fullscreen Mode?
	{
     
  如果我們仍然在全螢幕模式, 我們將設定延伸型式為 WS_EX_APPWINDOW, 當我們的視窗顯現時, 這樣會強制將視窗程式收到工作表上. 對於視窗形式, 我們將建立一個 WS_POPUP 視窗. 這種視窗類型沒有邊框, 很適合用在全螢幕模式下.

最後我們取消掉滑鼠指標. 如果你的程式不需要和使用者互動, 那通常在全螢幕模式下取消掉滑鼠指標是很好的. 這全看你的需要了.
 
     
		dwExStyle=WS_EX_APPWINDOW;					// Window Extended Style
		dwStyle=WS_POPUP;						// Windows Style
		ShowCursor(FALSE);						// Hide Mouse Pointer
	}
	else
	{
     
  如果我們使用視窗模式而非全螢幕模式, 我們會在延伸形式再加上 WS_EX_WINDOWEDGE. 這會讓視窗看起來更立體些. 至於視窗形式我們會用 WS_OVERLAPPEDWINDOW, 而非 WS_POPUP. WS_OVERLAPPEDWINDOW 建出的視窗會含有一個標題列, 可改變大小的邊框, 視窗選單, 以及最小化 / 最大化的按鈕.  
     
		dwExStyle=WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;			// Window Extended Style
		dwStyle=WS_OVERLAPPEDWINDOW;					// Windows Style
	}
     
  下列這一行依照我們所設定的視窗形式來調整視窗. 調整後會讓我們的視窗照我們所想要的解析度顯示. 通常邊框會覆蓋我們視窗的一小部份. 藉著使用 AdjustWindowRectEx 命令, 我們的 OpenGL 場景就不會被邊框覆蓋了, 不過為了要畫出邊框, 視窗會比較大一點. 在全螢幕模式下, 這個指令就沒有作用了.  
     
	AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle);		// Adjust Window To True Requested Size
     
  下一段程式碼中, 我們將建立我們的視窗, 並且檢查視窗是否正確的被建立了. 我們將傳入所有需要的參數到 CreateWindowEx(). 我們決定使用的延伸形式. 類別名稱 (使用和註冊視窗類別相同的名字). 視窗標題. 視窗形式. 視窗左上角的位置 (0,0 是一個安全的設定). 視窗的寬度和高度. 我們不想要有父視窗, 也不要選單, 所以這些參數都設為 NULL. 傳入我們的視窗 instance, 最後一個參數設為 NULL.

注意到我們在視窗形式上設了 WS_CLIPSIBLINGS 和 WS_CLIPCHILDREN. WS_CLIPSIBLINGS 和 WS_CLIPCHILDREN 是必須要的, 這樣 OpenGL 才能正常運作. 這些形式的設定可以避免別的視窗畫到我們的 OpenGL 視窗上.
 
     
	if (!(hWnd=CreateWindowEx(	dwExStyle,				// Extended Style For The Window
					"OpenGL",				// Class Name
					title,					// Window Title
					WS_CLIPSIBLINGS |			// Required Window Style
					WS_CLIPCHILDREN |			// Required Window Style
					dwStyle,				// Selected Window Style
					0, 0,					// Window Position
					WindowRect.right-WindowRect.left,	// Calculate Adjusted Window Width
					WindowRect.bottom-WindowRect.top,	// Calculate Adjusted Window Height
					NULL,					// No Parent Window
					NULL,					// No Menu
					hInstance,				// Instance
					NULL)))					// Don't Pass Anything To WM_CREATE
     
  接下來我們會檢查視窗是否正確的建立了. 如果我們的視窗被建立了, hWnd 會存放視窗的 handle. 如果視窗沒有建立, 那接下來的程式碼就會彈出一個錯誤訊息, 程式也就結束了.  
     
	{
		KillGLWindow();							// Reset The Display
		MessageBox(NULL,"Window Creation Error.","ERROR",MB_OK|MB_ICONEXCLAMATION);
		return FALSE;							// Return FALSE
	}
     
  接下來這一段程式碼描述一個圖素的格式. 我們選擇一個格式支援 OpenGL 和雙緩衝區, 在 RGBA (紅, 綠, 藍, 透明) 狀態. 我們嘗試著找一個圖素格式符合所選的色彩位元數 (16bit,24bit,32bit). 最後設定 16bit 的 Z-緩衝區. 剩下的參數不是沒用到就是不重要的 (像是模板 stencil 緩衝區和 (緩慢的) 累積 accumulation 緩衝區).  
     
	static	PIXELFORMATDESCRIPTOR pfd=					// pfd Tells Windows How We Want Things To Be
	{
		sizeof(PIXELFORMATDESCRIPTOR),					// Size Of This Pixel Format Descriptor
		1,								// Version Number
		PFD_DRAW_TO_WINDOW |						// Format Must Support Window
		PFD_SUPPORT_OPENGL |						// Format Must Support OpenGL
		PFD_DOUBLEBUFFER,						// Must Support Double Buffering
		PFD_TYPE_RGBA,							// Request An RGBA Format
		bits,								// Select Our Color Depth
		0, 0, 0, 0, 0, 0,						// Color Bits Ignored
		0,								// No Alpha Buffer
		0,								// Shift Bit Ignored
		0,								// No Accumulation Buffer
		0, 0, 0, 0,							// Accumulation Bits Ignored
		16,								// 16Bit Z-Buffer (Depth Buffer)
		0,								// No Stencil Buffer
		0,								// No Auxiliary Buffer
		PFD_MAIN_PLANE,							// Main Drawing Layer
		0,								// Reserved
		0, 0, 0								// Layer Masks Ignored
	};
     
  如果在建立視窗沒有錯誤的話, 我們會嘗試著得到一個 OpenGL 裝置內容 Device Context. 如果不能得到 DC, 那畫面上就會彈出一個錯誤訊息, 程式就會離開 (傳回 FALSE).  
     
	if (!(hDC=GetDC(hWnd)))							// Did We Get A Device Context?
	{
		KillGLWindow();							// Reset The Display
		MessageBox(NULL,"Can't Create A GL Device Context.","ERROR",MB_OK|MB_ICONEXCLAMATION);
		return FALSE;							// Return FALSE
	}
     
  如果我們準備得到一個裝置內容給 OpenGL 視窗, 那我們最會試著找到一個圖素格式符合我們上面所描述的. 如果視窗不能找到一個符合的圖素格式, 那畫面上就會彈出一個錯誤訊息, 程式就會離開 (傳回 FALSE).  
     
	if (!(PixelFormat=ChoosePixelFormat(hDC,&pfd)))				// Did Windows Find A Matching Pixel Format?
	{
		KillGLWindow();							// Reset The Display
		MessageBox(NULL,"Can't Find A Suitable PixelFormat.","ERROR",MB_OK|MB_ICONEXCLAMATION);
		return FALSE;							// Return FALSE
	}
     
  如果視窗找到一個符合的圖素格式, 我們就試著去設定圖素格式. 如果圖素格式不能設定, 那畫面上就會彈出一個錯誤訊息, 程式就會離開 (傳回 FALSE).  
     
	if(!SetPixelFormat(hDC,PixelFormat,&pfd))				// Are We Able To Set The Pixel Format?
	{
		KillGLWindow();							// Reset The Display
		MessageBox(NULL,"Can't Set The PixelFormat.","ERROR",MB_OK|MB_ICONEXCLAMATION);
		return FALSE;							// Return FALSE
	}
     
  如果圖素格式正確的設定了, 我們會試著得到著色區. 如果我們不能得到著色區, 那畫面上就會彈出一個錯誤訊息, 程式就會離開 (傳回 FALSE).  
     
	if (!(hRC=wglCreateContext(hDC)))					// Are We Able To Get A Rendering Context?
	{
		KillGLWindow();							// Reset The Display
		MessageBox(NULL,"Can't Create A GL Rendering Context.","ERROR",MB_OK|MB_ICONEXCLAMATION);
		return FALSE;							// Return FALSE
	}
     
  如果到目前為止都沒有錯誤, 那我們可以處理建好的裝置內容和著色區, 我們現在所要做的就是讓著色區運作. 如果我們不能啟動著色區, 那畫面上就會彈出一個錯誤訊息, 程式就會離開 (傳回 FALSE).  
     
	if(!wglMakeCurrent(hDC,hRC))						// Try To Activate The Rendering Context
	{
		KillGLWindow();							// Reset The Display
		MessageBox(NULL,"Can't Activate The GL Rendering Context.","ERROR",MB_OK|MB_ICONEXCLAMATION);
		return FALSE;							// Return FALSE
	}
     
  如果每件事都很順利, 那 OpenGL 視窗建好了後就要顯示出這個視窗, 把它設為前景作業視窗 (給較高的優先權), 然後將焦點設到這個視窗上. 接著我們將可以呼叫 ReSizeGLScene, 傳入畫面的寬高來設定我們具備透視觀點的 OpenGL 畫面.  
     
	ShowWindow(hWnd,SW_SHOW);						// Show The Window
	SetForegroundWindow(hWnd);						// Slightly Higher Priority
	SetFocus(hWnd);								// Sets Keyboard Focus To The Window
	ReSizeGLScene(width, height);						// Set Up Our Perspective GL Screen
     
  最後進入 InitGL(), 在這兒設定光源, 貼圖材質, 和任何其他需要設定的東西. 你可以在 InitGL() 作你要的錯誤檢查, 並傳出 TRUE (一切都正常時), 或是 FALSE (有一些不正確時). 例如, 如果你在 InitGL() 中載入貼圖材質, 卻有錯誤時, 你可能想要停止程式. 如果你由 InitGL() 傳回 FALSE, 那下面的程式碼就會把 FALSE 當成錯誤訊息, 並且結束程式.  
     
	if (!InitGL())								// Initialize Our Newly Created GL Window
	{
		KillGLWindow();							// Reset The Display
		MessageBox(NULL,"Initialization Failed.","ERROR",MB_OK|MB_ICONEXCLAMATION);
		return FALSE;							// Return FALSE
	}
     
  如果我們執行到這兒了, 就假定視窗已經成功的建立了. 我們傳回 TRUE 給 WinMain(), 來告訴 WinMain() 沒有問題了. 這樣程式就不會離開了.  
     
	return TRUE;								// Success
}
     
  這是處理所有視窗訊息的地方. 當我們註冊視窗類別, 我們告訴它要在這個地方處理視窗訊息.  
     
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
{
     
  下面的程式碼設定 uMsg, 在所有條件的狀態都要被比較出來. uMsg 會存放我們所要處理的訊息名稱.  
     
	switch (uMsg)								// Check For Windows Messages
	{
     
  如果 uMsg 是 WM_ACTIVE, 我們就檢查我們的視窗是否仍然在啟動著. 如果我們的視窗已經最小化了, 那變數 active 就要設成 FALSE. 如果我們的視窗在啟動著, 變數 active 就要設成 TRUE.  
     
		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
		}
     
  如果訊息是 WM_SYSCOMMAND (系統命令), 我們會以 wParam 作為條件式的分析. 如果 wParam 是 SC_SCREENSAVE 或 SC_MONITORPOWER, 那就表示電腦將進入螢幕保護模式或者是螢幕將進入省電模式. 那麼傳回 0, 就可以避免這兩種情況發生.  
     
		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
		}
     
  如果 uMsg 是 WM_CLOSE, 則視窗已經被關閉了. 我們送出一個離開訊息, 那麼主迴圈就會被中斷. 變數 done 將被設為 TRUE, WinMain() 的主要迴圈就會停止, 接著程式就關閉了.  
     
		case WM_CLOSE:							// Did We Receive A Close Message?
		{
			PostQuitMessage(0);					// Send A Quit Message
			return 0;						// Jump Back
		}
     
  如果一個按鍵被按下, 我們透過 wParam 的讀取找出是哪一個按鍵. 然後我將按鍵陣列 keys[] 的該鍵設為 TRUE. 那麼我就可以隨後透過按鍵陣列來找出哪些按鍵被按下. 這樣就允許數個按鍵在同時被按下了.  
     
		case WM_KEYDOWN:						// Is A Key Being Held Down?
		{
			keys[wParam] = TRUE;					// If So, Mark It As TRUE
			return 0;						// Jump Back
		}
     
  如果一個按鍵被放開, 我們透過 wParam 的讀取找出是哪一個按鍵. 然後我將按鍵陣列 keys[] 的該鍵設為 FALSE. 那麼我就可以透過讀取按鍵陣列來找出那個按鍵被按下或是它已經被放開了. 鍵盤上的各個按鍵可以用 0-255 中的其中一個數字表示. 例如: 當我按下一個按鍵的代表數字是 40 時, keys[40] 會被設為 TRUE. 放開後, 它就會被設為 FALSE. 這就是我們如何使用陣列來存放按鍵的按下狀態.  
     
		case WM_KEYUP:							// Has A Key Been Released?
		{
			keys[wParam] = FALSE;					// If So, Mark It As FALSE
			return 0;						// Jump Back
		}
     
  當我們改變視窗大小時, uMsg 則會變成訊息 WM_SIZE. 我們讀取 lParam 內低字組 (LOWORD) 和高字組 (HIWORD) 的值來找出視窗新的寬度和高度. 我們把新的寬度和高度傳入 ReSizeGLScene(). OpenGL 場景就會重設大小到新的寬度和高度.  
     
		case WM_SIZE:							// Resize The OpenGL Window
		{
			ReSizeGLScene(LOWORD(lParam),HIWORD(lParam));		// LoWord=Width, HiWord=Height
			return 0;						// Jump Back
		}
	}
     
  其他我們不在乎的訊息就傳入 DefWindowProc, 那視窗就會自行處理它了.  
     
	// 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 將被用來檢查是否有等待中的訊息需要被處理. 變數 done 的起始值是 FALSE. 表示我們的視窗程式還沒有執行完. 只要 done 還是 FALSE, 那程式就會繼續執行著. 一但 done 由 FALSE 變為 TRUE, 我們的程式就會馬上離開了.  
     
	MSG	msg;								// Windows Message Structure
	BOOL	done=FALSE;							// Bool Variable To Exit Loop
     
  這一段的程式碼則是可以隨意選擇的. 它會彈出一個訊息方塊來詢問使用者, 是否要在全螢幕模式下執行程式. 如果使用者按下 NO 按鈕, 那麼變數 fullscreen 就會由 TRUE 改變為 FALSE, 而程式就會在視窗模式下執行而不是全螢幕模式.  
     
	// 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
	}
     
  這就是我們如何建立 OpenGL 視窗. 我們傳入標題, 寬度, 高度, 色彩位元數, 以及 TRUE (全螢幕模式) 或是 FALSE (視窗模式) 給 CreateGLWindow. 那就是了! 我很高興這個程式碼如此簡潔. 如果程式視窗因為某種理由而無法建立, 那就會傳回 FALSE, 我們的程式也會立即退出 (傳回 0).  
     
	// Create Our OpenGL Window
	if (!CreateGLWindow("NeHe's OpenGL Framework",640,480,16,fullscreen))
	{
		return 0;							// Quit If Window Was Not Created
	}
     
  這是我們迴圈的開頭. 只要 done 等於 FALSE, 那迴圈就會繼續執行著.  
     
	while(!done)								// Loop That Runs Until done=TRUE
	{
     
  第一件我們要做的事就是檢查看看是否有任何視窗訊息在等待中. 藉著用 PeekMessage(), 我們可以檢查視窗訊息而不必暫停我們的程式. 有許多的程式使用 GetMessage(). 這也是可以啦, 不過使用 GetMessage() 的話, 你的程式並不會做任何事直到它收到一個繪圖 (paint) 訊息, 或是其他的視窗訊息.  
     
		if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))			// Is There A Message Waiting?
		{
     
  下一段的程式碼則是我們要檢查看看是否有一個離開 (quit) 訊息被發布出來. 如果目前的訊息是引發自 PostQuitMessage(0) 的 WM_QUIT 訊息, 那麼變數 done 就會被設為 TRUE, 接著就會讓程式離開了.  
     
			if (msg.message==WM_QUIT)				// Have We Received A Quit Message?
			{
				done=TRUE;					// If So done=TRUE
			}
			else							// If Not, Deal With Window Messages
			{
     
  如果這個訊息不是離開 (quit) 訊息, 我們就會轉譯這個訊息, 然後發送出該訊息, 那麼 WndProc() 或是視窗作業系統就會處理它.  
     
				TranslateMessage(&msg);				// Translate The Message
				DispatchMessage(&msg);				// Dispatch The Message
			}
		}
		else								// If There Are No Messages
		{
     
  如果沒有訊息, 我們就會繪製我們的 OpenGL 場景. 下列第一行就是檢查看看視窗程式是不是在啟動中. 場景會被繪製出, 而傳回值也會被檢查著. 如果 DrawGLScene() 傳回 FALSE, 或是 ESC 按鍵被按下, 那麼變數 done 就會設為 TRUE, 接著導致程式的結束.  
     
			// Draw The Scene.  Watch For ESC Key And Quit Messages From DrawGLScene()
			if ((active && !DrawGLScene()) || keys[VK_ESCAPE])	// Updating View Only If Active
			{
				done=TRUE;					// ESC or DrawGLScene Signalled A Quit
			}
			else							// Not Time To Quit, Update Screen
			{
     
  如果一切的繪製都沒問題, 我們就切換緩衝區 (藉著使用雙緩衝區, 我們可以得到平滑不閃爍的動畫). 藉著使用雙緩衝區, 我們會在看不到的隱藏畫面中繪製一切. 當我們切換緩衝區時, 我們所看到的畫面就會是隱藏畫面, 所以之前的隱藏畫面的就會可以看到了. 用這個方式我們就不會看到正在繪製中的場景, 場景一下子就被畫好了.  
     
				SwapBuffers(hDC);				// Swap Buffers (Double Buffering)
			}
     
  下面的程式碼是新的, 最近才被加上去的 (05-01-00). 它允許我們按下 F1 按鍵來切換全螢幕模式到視窗模式, 或是視窗模式到全螢幕模式.  
     
			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 OpenGL Framework",640,480,16,fullscreen))
				{
					return 0;				// Quit If Window Was Not Created
				}
			}
		}
	}
     
  如果 done 變數不再是 FALSE, 程式就會離開. 我們會正確的殺掉 OpenGL 視窗, 所以一切的東西都要釋放掉, 然會離開程式.  
     
	// Shutdown
	KillGLWindow();								// Kill The Window
	return (msg.wParam);							// Exit The Program
}
     
  在這個課程中, 我已經試著很仔細的解釋了, 每一個步驟包含設定和建立一個你自己的全螢幕視窗; 而按下 ESC 鍵會離開程式; 並且監看視窗程式是否還被啟動著. 我大約花了兩個星期寫這個程式碼, 一個星期修正臭蟲 & 和程式高手談話, 以及兩天 (大約 22 小時) 寫這個 HTML 檔. 如果你有意見或是問題, 請寄電子郵件告訴我. 如果你認為我的註解不正確, 或者某些段落的程式可以再更好, 請讓我知道. 我想盡我最大努力作最好的 OpenGL 教學課程, 我很有興趣聽聽你們的反應.

Jeff Molofee (NeHe)

* 下載 Visual C++ 程式碼給本課程的.
* 下載 Delphi 程式碼給本課程的. ( Conversion by Peter De Jaegher )
* 下載 ASM 程式碼給本課程的. ( Conversion by Foolman )
* 下載 Visual Fortran 程式碼給本課程的. ( Conversion by Jean-Philippe Perois )
* 下載 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 Code For This Lesson. ( Conversion by Chris Herborth )
* 下載 Java 程式碼給本課程的. ( Conversion by Darren Hodges )
* 下載 Borland C++ Builder 4.0 程式碼給本課程的. ( Conversion by Patrick Salmons )
* 下載 MingW32 & Allegro 程式碼給本課程的. ( Conversion by Peter Puck )
* 下載 Python 程式碼給本課程的. ( Conversion by John )
 
     
 
Back To NeHe Productions!
回到 OpenGL 教學索引
中文版由 Macbear 翻譯