歡迎來(lái)到我的OpenGL教程。我是個(gè)對(duì)OpenGL充滿激情的普通男孩。我第一次聽(tīng)說(shuō)OpenGL是3Dfx發(fā)布Voodoo1卡的OpenGL硬件加速驅(qū)動(dòng)的時(shí)候。我立刻意識(shí)到OpenGL是那種必須學(xué)習(xí)的東西。不幸的是當(dāng)時(shí)很難從書本或網(wǎng)絡(luò)上找到關(guān)于OpenGL的訊息。我花了N個(gè)小時(shí)來(lái)調(diào)試自己書寫的代碼,甚至在IRC和e-Mail上花更多的時(shí)間來(lái)懇求別人幫忙。但我發(fā)現(xiàn)那些懂得OpenGL高手們保留了他們的精華,對(duì)共享知識(shí)也不感興趣。實(shí)在讓人灰心。
我創(chuàng)建這個(gè)網(wǎng)站的目的是為了
幫助那些對(duì)OpenGL有興趣卻又需要幫助的人。在我的每個(gè)教程中,我都會(huì)盡可能詳細(xì)的來(lái)解釋每一行代碼的作用。我會(huì)努力讓我的代碼更簡(jiǎn)單(您無(wú)需學(xué)習(xí)MFC代碼)。就算您是個(gè)VC、OpenGL的絕對(duì)
新手也應(yīng)該可以讀通代碼,并清楚的知道發(fā)生了什么。我的站點(diǎn)只是許多提供OpenGL教程的站點(diǎn)中的一個(gè)。如果您是OpenGL的高級(jí)程序員的話,我的站點(diǎn)可能太簡(jiǎn)單了,但如果您才開(kāi)始的話,我想這個(gè)站點(diǎn)會(huì)教會(huì)您許多東西。
教程的這一節(jié)在2000年一月徹底重寫了一遍。將會(huì)教您如何設(shè)置一個(gè)OpenGL窗口。它可以只是一個(gè)窗口或是全屏幕的、可以任意大小、任意色彩深度。此處的代碼很穩(wěn)定且很強(qiáng)大,您可以在您所有的OpenGL項(xiàng)目中使用。我所有的教程都將基于此節(jié)的代碼。所有的錯(cuò)誤都有被報(bào)告。所以應(yīng)該沒(méi)有內(nèi)存泄漏,代碼也很容易閱讀和修改。感謝Fredric Echols對(duì)代碼所做的修改。
現(xiàn)在就讓我們直接從代碼開(kāi)始吧。第一件事是打開(kāi)VC然后創(chuàng)建一個(gè)新工程。如果您不知道如何創(chuàng)建的話,您也許不該學(xué)習(xí)OpenGL,而應(yīng)該先學(xué)學(xué)VC。文末可供下載的代碼是VC++ 6.0的。某些版本的VC需要將
bool改成
BOOL,
true改成
TRUE,
false改成
FALSE,請(qǐng)自行修改。我用VC4和VC5編譯過(guò)這些代碼,沒(méi)有發(fā)現(xiàn)問(wèn)題。
在您創(chuàng)建一個(gè)新的Win32程序(
不是console控制臺(tái)程序)后,您還需要鏈接OpenGL庫(kù)文件。在VC中操作如下:Project > Settings,然后單擊LINK標(biāo)簽。在“Object/Library Modules”選項(xiàng)中的開(kāi)始處(在kernel32.lib 前)增加
OpenGL32.lib、
GLu32.lib及
GLaux.lib后單擊OK按鈕。現(xiàn)在可以開(kāi)始寫您的OpenGL程序了。
代碼的前4行包括了我們使用的每個(gè)庫(kù)文件的頭文件。如下所示:
#include <windows.h> // Windows的頭文件
#include <gl\gl.h> // OpenGL32庫(kù)的頭文件
#include <gl\glu.h> // GLu32庫(kù)的頭文件
#include <gl\glaux.h> // GLaux庫(kù)的頭文件
接下來(lái)您需要設(shè)置您計(jì)劃在您的程序中使用的所有變量。本節(jié)中的例程將創(chuàng)建一個(gè)空的OpenGL窗口,因此我們暫時(shí)還無(wú)需設(shè)置大堆的變量。余下需要設(shè)置的變量不多,但十分重要。您將會(huì)在您以后所寫的每一個(gè)OpenGL程序中用到它們。
第一行設(shè)置的變量是著色描述表(
Rendering Context)。每一個(gè)OpenGL都被連接到一個(gè)著色描述表上。著色描述表將所有的OpenGL調(diào)用命令連接到設(shè)備描述表(
Device Context)上。我將OpenGL的著色描述表定義為
hRC。要讓您的程序能夠繪制窗口的話,還需要?jiǎng)?chuàng)建一個(gè)設(shè)備描述表,也就是第二行的內(nèi)容。Windows的設(shè)備描述表被定義為
hDC。DC將窗口連接到圖形設(shè)備接口GDI(
Graphics Device Interface)。而RC將OpenGL連接到DC。第三行的變量
hWnd將保存由Windows給我們的窗口指派的句柄。最后,第四行為我們的程序創(chuàng)建了一個(gè)Instance(實(shí)例)。
HGLRC hRC=NULL; // 永久著色描述表 HDC hDC=NULL; // 私有GDI設(shè)備描述表 HWND hWnd=NULL; // 保存我們的窗口句柄 HINSTANCE hInstance; // 保存程序的實(shí)例 下面的第一行設(shè)置一個(gè)用來(lái)監(jiān)控鍵盤動(dòng)作的數(shù)組。有許多方法可以監(jiān)控鍵盤的動(dòng)作,但這里的方法很可靠,并且可以處理多個(gè)鍵同時(shí)按下的情況。
active變量用來(lái)告知程序窗口是否處于最小化的狀態(tài)。如果窗口已經(jīng)最小化的話,我們可以做從暫停代碼執(zhí)行到退出程序的任何事情。我喜歡暫停程序。這樣可以使得程序不用在后臺(tái)保持運(yùn)行。
fullscreen變量的作用相當(dāng)明顯。如果我們的程序在全屏狀態(tài)下運(yùn)行,
fullscreen的值為TRUE,否則為FALSE。這個(gè)全局變量的設(shè)置十分重要,它讓每個(gè)過(guò)程都知道程序是否運(yùn)行在全屏狀態(tài)下。
bool keys[256]; // 用于鍵盤例程的數(shù)組 bool active=TRUE; // 窗口的活動(dòng)標(biāo)志,缺省為TRUE bool fullscreen=TRUE; // 全屏標(biāo)志缺省設(shè)定成全屏模式 現(xiàn)在我們需要先定義WndProc()。必須這么做的原因是CreateGLWindow()有對(duì)WndProc()的引用,但WndProc()在CreateGLWindow()之后才出現(xiàn)。在C語(yǔ)言中,如果我們想要訪問(wèn)一個(gè)當(dāng)前程序段之后的過(guò)程和程序段的話,必須在程序開(kāi)始處先申明所要訪問(wèn)的程序段。所以下面的一行代碼先行定義了WndProc(),使得CreateGLWindow()能夠引用WndProc()。
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // WndProc的定義 下面的代碼的作用是重新設(shè)置OpenGL場(chǎng)景的大小,而不管窗口的大小是否已經(jīng)改變(假定您沒(méi)有使用全屏模式)。甚至您無(wú)法改變窗口的大小時(shí)(例如您在全屏模式下),它至少仍將運(yùn)行一次 — 在程序開(kāi)始時(shí)設(shè)置我們的透視圖。OpenGL場(chǎng)景的尺寸將被設(shè)置成它顯示時(shí)所在窗口的大小。
GLvoid ReSizeGLScene(GLsizei width, GLsizei height) // 重置并初始?疓L窗口大小
{
if (height==0) // 防止被零除
{
height=1; // 將Height設(shè)為1
}
glViewport(0, 0, width, height); // 重置當(dāng)前的視口(Viewport)
下面幾行為透視圖設(shè)置屏幕。意味著越遠(yuǎn)的東西看起來(lái)越小。這么做創(chuàng)建了一個(gè)現(xiàn)實(shí)外觀的場(chǎng)景。此處透視按照基于窗口寬度和高度的45度視角來(lái)計(jì)算。0.1f,100.0f是我們?cè)趫?chǎng)景中所能繪制深度的起點(diǎn)和終點(diǎn)。
glMatrixMode(GL_PROJECTION)指明接下來(lái)的兩行代碼將影響投影矩陣(
projection matrix)。投影矩陣負(fù)責(zé)為我們的場(chǎng)景增加透視。glLoadIdentity()近似于重置。它將所選的矩陣狀態(tài)恢復(fù)成其原始狀態(tài)。調(diào)用 glLoadIdentity()之后我們?yōu)閳?chǎng)景設(shè)置透視圖。glMatrixMode(GL_MODELVIEW)指明任何新的變換將會(huì)影響模型觀察矩陣(
modelview matrix)。模型觀察矩陣中存放了我們的物體訊息。最后我們重置模型觀察矩陣。如果您還不能理解這些術(shù)語(yǔ)的含義,請(qǐng)別著急。在以后的教程里,我會(huì)向大家解釋。只要知道如果您想獲得一個(gè)精彩的透視場(chǎng)景的話,
必須這么做。
glMatrixMode(GL_PROJECTION); // 選擇投影矩陣
glLoadIdentity(); // 重置投影矩陣
// 計(jì)算窗口的外觀比例
gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,0.1f,100.0f); glMatrixMode(GL_MODELVIEW); // 選擇模型觀察矩陣
glLoadIdentity(); // 重置模型觀察矩陣
}
接下的代碼段中,我們將對(duì)OpenGL進(jìn)行所有的設(shè)置。我們將設(shè)置清除屏幕所用的顏色,打開(kāi)深度緩存,啟用陰影平滑(
smooth shading),等等。這個(gè)例程直到OpenGL窗口創(chuàng)建之后才會(huì)被調(diào)用。此過(guò)程將有返回值。但我們此處的初始化沒(méi)那么復(fù)雜,現(xiàn)在還用不著擔(dān)心這個(gè)返回值。
int InitGL(GLvoid) // 此處開(kāi)始對(duì)OpenGL進(jìn)行所有設(shè)置
{ 下一行啟用陰影平滑(
smooth shading)。陰影平滑通過(guò)多邊形精細(xì)的混合色彩,并對(duì)外部光進(jìn)行平滑。我將在另一個(gè)教程中更詳細(xì)的解釋陰影平滑。
glShadeModel(GL_SMOOTH); // 啟用陰影平滑 下一行設(shè)置清除屏幕時(shí)所用的顏色。如果您對(duì)色彩的工作原理不清楚的話,我快速解釋一下。色彩值的范圍從0.0f到1.0f。0.0f代表最黑的情況,1.0f就是最亮的情況。glClearColor后的第一個(gè)參數(shù)是紅色分量(
Red Intensity),第二個(gè)是綠色,第三個(gè)是藍(lán)色。最大值也是1.0f,代表特定顏色分量的最亮情況。最后一個(gè)參數(shù)是Alpha值。當(dāng)它用來(lái)清除屏幕的時(shí)候,我們不用關(guān)心第四個(gè)數(shù)字。現(xiàn)在讓它為0.0f。我會(huì)用另一個(gè)教程來(lái)解釋這個(gè)參數(shù)。
通過(guò)混合三種原色(紅、綠、藍(lán)),您可以得到不同的色彩。希望您在學(xué)校里學(xué)過(guò)這些。因此,當(dāng)您使用glClearColor(0.0f,0.0f,1.0f,0.0f),您將用亮藍(lán)色來(lái)清除屏幕。如果您用 glClearColor(0.5f,0.0f,0.0f,0.0f)的話,您將使用中紅色來(lái)清除屏幕。不是最亮(1.0f),也不是最暗(0.0f)。要得到白色背景,您應(yīng)該將所有的顏色設(shè)成最亮(1.0f)。要黑色背景的話,您該將所有的顏色設(shè)為最暗(0.0f)。
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // 黑色背景 接下來(lái)的三行必須做的是關(guān)于深度緩存(
depth buffer)的。將深度緩存設(shè)想為屏幕后面的層?I疃然捍娌歡系畝暈鍰褰肫聊荒誆坑卸嗌罱懈佟N頤潛窘詰某絳蚱涫得揮姓嬲褂蒙疃然捍媯負(fù)跛性諂聊簧舷允?3D場(chǎng)景OpenGL程序都使用深度緩存。它的排序決定那個(gè)物體先畫。這樣您就不會(huì)將一個(gè)圓形后面的正方形畫到圓形上來(lái)。深度緩存是OpenGL十分重要的部分。
glClearDepth(1.0f); // 設(shè)置深度緩存 glEnable(GL_DEPTH_TEST); // 啟用深度測(cè)試 glDepthFunc(GL_LEQUAL); // 所作深度測(cè)試的類型 接著告訴OpenGL我們希望進(jìn)行最好的透視修正。這會(huì)十分輕微的影響性能。但使得透視圖看起來(lái)好一點(diǎn)。
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // 真正精細(xì)的透視修正 最后,我們返回TRUE。如果我們希望檢查初始化是否OK,我們可以查看返回的 TRUE或FALSE的值。如果有錯(cuò)誤發(fā)生的話,您可以加上您自己的代碼返回FALSE。目前,我們不管它。
return TRUE; // 初始化 OK } 下一段包括了所有的繪圖代碼。任何您所想在屏幕上顯示的東東都將在此段代碼中出現(xiàn)。以后的每個(gè)教程中我都會(huì)在例程的此處增加新的代碼。如果您對(duì)OpenGL已經(jīng)有所了解的話,您可以在 glLoadIdentity()調(diào)用之后,返回TRUE值之前,試著添加一些OpenGL代碼來(lái)創(chuàng)建基本的形。如果您是OpenGL新手,等著我的下個(gè)教程。目前我們所作的全部就是將屏幕清除成我們前面所決定的顏色,清除深度緩存并且重置場(chǎng)景。我們?nèi)詻](méi)有繪制任何東東。
返回TRUE值告知我們的程序沒(méi)有出現(xiàn)問(wèn)題。如果您希望程序因?yàn)槟承┰蚨兄惯\(yùn)行,在返回TRUE值之前增加返回FALSE的代碼告知我們的程序繪圖代碼出錯(cuò)。程序即將退出。
int DrawGLScene(GLvoid) // 從這里開(kāi)始進(jìn)行所有的繪制 {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// 清除屏幕和深度緩存 glLoadIdentity(); // 重置當(dāng)前的模型觀察矩陣 return TRUE; // 一切 OK } 下一段代碼只在程序退出之前調(diào)用。KillGLWindow()的作用是依次釋放著色描述表,設(shè)備描述表和窗口句柄。我已經(jīng)加入了許多錯(cuò)誤檢查。如果程序無(wú)法銷毀窗口的任意部分,都會(huì)彈出帶相應(yīng)錯(cuò)誤消息的訊息窗口,告訴您什么出錯(cuò)了。使您在您的代碼中查錯(cuò)變得更容易些。
GLvoid KillGLWindow(GLvoid) // 正常銷毀窗口 { 我們?cè)贙illGLWindow()中所作的第一件事是檢查我們是否處于全屏模式。如果是,我們要切換回桌面。我們本應(yīng)在禁用全屏模式前先銷毀窗口,但在某些顯卡上這么做可能會(huì)使得桌面崩潰。所以我們還是先禁用全屏模式。這將防止桌面出現(xiàn)崩潰,并在Nvidia和3DFX顯卡上都工作的很好。
if (fullscreen) // 我們處于全屏模式嗎? { 我們使用ChangeDisplaySettings(NULL,0)回到原始桌面。將NULL作為第一個(gè)參數(shù),0作為第二個(gè)參數(shù)傳遞強(qiáng)制Windows使用當(dāng)前存放在注冊(cè)表中的值(缺省的分辨率、色彩深度、刷新頻率,等等)來(lái)有效的恢復(fù)我們的原始桌面。切換回桌面后,我們還要使得鼠標(biāo)指針重新可見(jiàn)。
ChangeDisplaySettings(NULL,0); // 是的話,切換回桌面 ShowCursor(TRUE); // 顯示鼠標(biāo)指針 } 接下來(lái)的代碼查看我們是否擁有著色描述表(hRC)。如果沒(méi)有,程序?qū)⑻D(zhuǎn)至后面的代碼查看是否擁有設(shè)備描述表。
if (hRC) // 我們擁有著色描述表嗎? { 如果存在著色描述表的話,下面的代碼將查看我們能否釋放它(將
hRC從
hDC分開(kāi))。這里請(qǐng)注意我使用的的查錯(cuò)方法。基本上我只是讓程序嘗試釋放著色描述表(通過(guò)調(diào)用:wglMakeCurrent(NULL,NULL)),然后我再查看釋放是否成功。巧妙的將數(shù)行代碼結(jié)合到了一行。
if (!wglMakeCurrent(NULL,NULL)) // 我們能否釋放DC和RC描述表? {
如果不能釋放DC和RC描述表的話,MessageBox()將彈出錯(cuò)誤消息,告知我們DC和RC無(wú)法被釋放。NULL意味著消息窗口沒(méi)有父窗口。其右的文字將在消息窗口上出現(xiàn)。“SHUTDOWN ERROR”出現(xiàn)在窗口的標(biāo)題欄上。MB_OK的意思消息窗口上帶有一個(gè)寫著OK字樣的按鈕。
MB_ICONINFORMATION將在消息窗口中顯示一個(gè)帶圈的小寫的i(看上去更正式一些)。
MessageBox(NULL,"Release Of DC And RC Failed.",
"SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
} 下一步我們?cè)囍鴦h除著色描述表。如果不成功的話彈出錯(cuò)誤消息。
if (!wglDeleteContext(hRC)) // 我們能否刪除RC? { 如果無(wú)法刪除著色描述表的話,將彈出錯(cuò)誤消息告知我們RC未能成功刪除。然后
hRC被設(shè)為NULL。
MessageBox(NULL,"Release Rendering Context Failed.",
"SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
}
hRC=NULL; // 將RC設(shè)為 NULL } 現(xiàn)在我們查看是否存在設(shè)備描述表,如果有嘗試釋放它。如果不能釋放設(shè)備描述表將彈出錯(cuò)誤消息,然后
hDC設(shè)為NULL。
if (hDC && !ReleaseDC(hWnd,hDC)) // 我們能否釋放 DC?
{
MessageBox(NULL,"Release Device Context Failed.",
"SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
hDC=NULL; // 將 DC 設(shè)為 NULL
}
現(xiàn)在我們來(lái)查看是否存在窗口句柄,我們調(diào)用DestroyWindow(
hWnd)來(lái)嘗試銷毀窗口。如果不能的話彈出錯(cuò)誤窗口,然后
hWnd被設(shè)為NULL。
if (hWnd && !DestroyWindow(hWnd)) // 能否銷毀窗口?
{
MessageBox(NULL,"Could Not Release hWnd.",
"SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
hWnd=NULL; // 將 hWnd 設(shè)為 NULL } 最后要做的事是注銷我們的窗口類。這允許我們正常銷毀窗口,接著在打開(kāi)其他窗口時(shí),不會(huì)收到諸如“Windows Class already registered”(窗口類已注冊(cè))的錯(cuò)誤消息。
if (!UnregisterClass("OpenGL",hInstance)) // 能否注銷類? {
MessageBox(NULL,"Could Not Unregister Class.",
"SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
hInstance=NULL; // 將 hInstance 設(shè)為 NULL
}
}
接下來(lái)的代碼段創(chuàng)建我們的OpenGL窗口。我花了很多時(shí)間來(lái)做決定是否創(chuàng)建固定的全屏模式這樣不需要許多額外的代碼,還是創(chuàng)建一個(gè)容易定制的友好的窗口但需要更多的代碼。當(dāng)然最后我選擇了后者。我經(jīng)常在EMail中收到諸如此類的問(wèn)題:怎樣創(chuàng)建窗口而不使用全屏幕?怎樣改變窗口的標(biāo)題欄?怎樣改變窗口的分辨率或象素格式(
pixel format)?以下的代碼完成了所有這一切。盡管最好要學(xué)學(xué)材質(zhì),這會(huì)讓您寫自己的OpenGL程序變得容易的多。
正如您所見(jiàn),此過(guò)程返回布爾變量(TRUE或FALSE)。他還帶有5個(gè)參數(shù):窗口的
標(biāo)題欄,窗口的
寬度,窗口的
高度,
色彩位數(shù)(16/24/32),和
全屏標(biāo)志(TRUE — 全屏模式,F(xiàn)ALSE — 窗口模式)。返回的布爾值告訴我們窗口是否成功創(chuàng)建。
BOOL CreateGLWindow(char* title, int width, int height, int bits, bool fullscreenflag)
{
當(dāng)我們要求Windows為我們尋找相匹配的象素格式時(shí),Windows尋找結(jié)束后將模式值保存在變量
PixelFormat中。
GLuint PixelFormat; // 保存查找匹配的結(jié)果
wc用來(lái)保存我們的窗口類的結(jié)構(gòu)。窗口類結(jié)構(gòu)中保存著我們的窗口信息。通過(guò)改變類的不同字段我們可以改變窗口的外觀和行為。每個(gè)窗口都屬于一個(gè)窗口類。當(dāng)您創(chuàng)建窗口時(shí),您必須為窗口注冊(cè)類。
WNDCLASS wc; // 窗口類結(jié)構(gòu) dwExStyle和
dwStyle存放擴(kuò)展和通常的窗口風(fēng)格信息。我使用變量來(lái)存放風(fēng)格的目的是為了能夠根據(jù)我需要?jiǎng)?chuàng)建的窗口類型(是全屏幕下的彈出窗口還是窗口模式下的帶邊框的普通窗口);來(lái)改變窗口的風(fēng)格。
DWORD dwExStyle; // 擴(kuò)展窗口風(fēng)格
DWORD dwStyle; // 窗口風(fēng)格
下面的5行代碼取得矩形的左上角和右下角的坐標(biāo)值。我們將使用這些值來(lái)調(diào)整我們的窗口使得其上的繪圖區(qū)的大小恰好是我們所需的分辨率的值。通常如果我們創(chuàng)建一個(gè)640x480的窗口,窗口的邊框會(huì)占掉一些分辨率的值。
RECT WindowRect; // 取得矩形的左上角和右下角的坐標(biāo)值
WindowRect.left=(long)0; // 將Left 設(shè)為 0
WindowRect.right=(long)width; // 將Right 設(shè)為要求的寬度
WindowRect.top=(long)0; // 將Top 設(shè)為 0
WindowRect.bottom=(long)height; // 將Bottom 設(shè)為要求的高度
下一行代碼我們讓全局變量
fullscreen等于
fullscreenflag。如果我們希望在全屏幕下運(yùn)行而將
fullscreenflag設(shè)為TRUE,但沒(méi)有讓變量
fullscreen等于
fullscreenflag的話,
fullscreen變量將保持為FALSE。當(dāng)我們?cè)谌聊荒J较落N毀窗口的時(shí)候,變量fullscreen的值卻不是正確的TRUE值,計(jì)算機(jī)將誤以為已經(jīng)處于桌面模式而無(wú)法切換回桌面。上帝啊,但愿這一切都有意義。就是一句話,
fullscreen的值必須永遠(yuǎn)
fullscreenflag的值,否則就會(huì)有問(wèn)題。(
CKER也覺(jué)得此處太廢話,懂的人都要不懂啦.....:()
fullscreen=fullscreenflag; // 設(shè)置全局全屏標(biāo)志 下一部分的代碼中,我們?nèi)〉么翱诘膶?shí)例,然后定義窗口類。
CS_HREDRAW和CS_VREDRAW 的意思是無(wú)論何時(shí),只要窗口發(fā)生變化時(shí)就強(qiáng)制重畫。CS_OWNDC為窗口創(chuàng)建一個(gè)私有的DC。這意味著DC不能在程序間共享。WndProc是我們程序的消息處理過(guò)程。由于沒(méi)有使用額外的窗口數(shù)據(jù),后兩個(gè)字段設(shè)為零。然后設(shè)置實(shí)例。接著我們將hIcon設(shè)為NULL,因?yàn)槲覀儾幌虢o窗口來(lái)個(gè)圖標(biāo)。鼠標(biāo)指針設(shè)為標(biāo)準(zhǔn)的箭頭。背景色無(wú)所謂(我們?cè)贕L中設(shè)置)。我們也不想要窗口菜單,所以將其設(shè)為NULL。類的名字可以您想要的任何名字。出于簡(jiǎn)單,我將使用“OpenGL”。
hInstance = GetModuleHandle(NULL); // 取得我們窗口的實(shí)例
wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;// 移動(dòng)時(shí)重畫,并為窗口取得DC
wc.lpfnWndProc = (WNDPROC) WndProc; // WndProc處理消息
wc.cbClsExtra = 0; // 無(wú)額外窗口數(shù)據(jù)
wc.cbWndExtra = 0; // 無(wú)額外窗口數(shù)據(jù)
wc.hInstance = hInstance; // 設(shè)置實(shí)例
wc.hIcon = LoadIcon(NULL, IDI_WINLOGO); // 裝入缺省圖標(biāo)
wc.hCursor = LoadCursor(NULL, IDC_ARROW); // 裝入鼠標(biāo)指針
wc.hbrBackground = NULL; // GL不需要背景
wc.lpszMenuName = NULL; // 不需要菜單
wc.lpszClassName = "OpenGL"; // 設(shè)定類名字
現(xiàn)在注冊(cè)類名字。如果有錯(cuò)誤發(fā)生,彈出錯(cuò)誤消息窗口。按下上面的OK按鈕后,程序退出。
if (!RegisterClass(&wc)) // 嘗試注冊(cè)窗口類
{
MessageBox(NULL,"Failed To Register The Window Class.","ERROR",
MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 退出并返回FALSE
}
查看程序應(yīng)該在全屏模式還是窗口模式下運(yùn)行。如果應(yīng)該是全屏模式的話,我們將嘗試設(shè)置全屏模式。
if (fullscreen) // 要嘗試全屏模式嗎? { 下一部分的代碼看來(lái)很多人都會(huì)有問(wèn)題要問(wèn)關(guān)于......切換到全屏模式。在切換到全屏模式時(shí),有幾件十分重要的事您必須牢記。必須確保您在全屏模式下所用的寬度和高度等同于窗口模式下的寬度和高度。
最最重要的是要在創(chuàng)建窗口之前設(shè)置全屏模式。這里的代碼中,您無(wú)需再擔(dān)心寬度和高度,它們已被設(shè)置成與顯示模式所對(duì)應(yīng)的大小。
DEVMODE dmScreenSettings; // 設(shè)備模式
memset(&dmScreenSettings,0,sizeof(dmScreenSettings)); // 確保內(nèi)存分配
dmScreenSettings.dmSize=sizeof(dmScreenSettings); // Devmode 結(jié)構(gòu)的大小
dmScreenSettings.dmPelsWidth = width; // 所選屏幕寬度
dmScreenSettings.dmPelsHeight = height; // 所選屏幕高度
dmScreenSettings.dmBitsPerPel = bits; // 每象素所選的色彩深度
dmScreenSettings.dmFields = DM_BITSPERPEL|DM_PELSWIDTH|DM_PELSHEIGHT; 上面的代碼中,我們分配了用于存儲(chǔ)
視頻設(shè)置的空間。設(shè)定了屏幕的寬,高,色彩深度。下面的代碼我們嘗試設(shè)置全屏模式。我們?cè)赿mScreenSettings中保存了所有的寬,高,色彩深度訊息。下一行使用ChangeDisplaySettings來(lái)嘗試切換成與dmScreenSettings所匹配模式。我使用參數(shù)CDS_FULLSCREEN來(lái)切換顯示模式,因?yàn)檫@樣做不僅移去了屏幕底部的狀態(tài)條,而且它在來(lái)回切換時(shí),沒(méi)有移動(dòng)或改變您在桌面上的窗口。
// 嘗試設(shè)置顯示模式并返回結(jié)果。注: CDS_FULLSCREEN 移去了狀態(tài)條。
if (ChangeDisplaySettings(&dmScreenSettings,
CDS_FULLSCREEN)!=DISP_CHANGE_SUCCESSFUL)
{ 如果模式未能設(shè)置成功,我們將進(jìn)入以下的代碼。如果不能匹配全屏模式,彈出消息窗口,提供兩個(gè)選項(xiàng):在窗口模式下運(yùn)行或退出。
// 若模式失敗,提供兩個(gè)選項(xiàng):退出或在窗口內(nèi)運(yùn)行。 if (MessageBox(NULL,
"The Requested Fullscreen Mode Is Not Supported By\n
Your Video Card. Use Windowed Mode Instead?","NeHe GL",
MB_YESNO|MB_ICONEXCLAMATION)==IDYES)
{ 如果用戶選擇窗口模式,變量
fullscreen的值變?yōu)镕ALSE,程序繼續(xù)運(yùn)行。
fullscreen=FALSE; // 選擇窗口模式(Fullscreen=FALSE)
}
else
{
如果用戶選擇退出,彈出消息窗口告知用戶程序?qū)⒔Y(jié)束。并返回FALSE告訴程序窗口未能成功創(chuàng)建。程序退出。
// 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; //退出并返回 FALSE
}
}
}
由于全屏模式可能失敗,用戶可能決定在窗口下運(yùn)行,我們需要在設(shè)置屏幕/窗口之前,再次檢查
fullscreen的值是TRUE或FALSE。
if (fullscreen) // 仍處于全屏模式嗎? {
如果我們?nèi)蕴幱谌聊J剑O(shè)置擴(kuò)展窗體風(fēng)格為WS_EX_APPWINDOW,這將強(qiáng)制我們的窗體可見(jiàn)時(shí)處于最前面。再將窗體的風(fēng)格設(shè)為WS_POPUP。這個(gè)類型的窗體沒(méi)有邊框,使我們的全屏模式得以完美顯示。
最后我們禁用鼠標(biāo)指針。當(dāng)您的程序不是交互式的時(shí)候,在全屏模式下禁用鼠標(biāo)指針通常是個(gè)好主意。
dwExStyle=WS_EX_APPWINDOW; // 擴(kuò)展窗體風(fēng)格
dwStyle=WS_POPUP; // 窗體風(fēng)格
ShowCursor(FALSE); // 隱藏鼠標(biāo)指針
}
else
{
如果我們使用窗口而不是全屏模式,我們?cè)跀U(kuò)展窗體風(fēng)格中增加了 WS_EX_WINDOWEDGE,增強(qiáng)窗體的3D感觀。窗體風(fēng)格改用 WS_OVERLAPPEDWINDOW,創(chuàng)建一個(gè)帶標(biāo)題欄、可變大小的邊框、菜單和最大化/最小化按鈕的窗體。
dwExStyle=WS_EX_APPWINDOW | WS_EX_WINDOWEDGE; // 擴(kuò)展窗體風(fēng)格
dwStyle=WS_OVERLAPPEDWINDOW; // 窗體風(fēng)格
}
下一行代碼根據(jù)創(chuàng)建的窗體類型調(diào)整窗口。調(diào)整的目的是使得窗口大小正好等于我們要求的分辨率。通常邊框會(huì)占用窗口的一部分。使用AdjustWindowRectEx 后,我們的OpenGL場(chǎng)景就不會(huì)被邊框蓋住。實(shí)際上窗口變得更大以便繪制邊框。全屏模式下,此命令無(wú)效。
AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle);// 調(diào)整窗口達(dá)到真正要求的大小 下一段代碼開(kāi)始創(chuàng)建窗口并檢查窗口是否成功創(chuàng)建。我們將傳遞CreateWindowEx()所需的所有參數(shù)。如擴(kuò)展風(fēng)格、類名字(與您在注冊(cè)窗口類時(shí)所用的名字相同)、窗口標(biāo)題、窗體風(fēng)格、窗體的左上角坐標(biāo)(0,0是個(gè)安全的選擇)、窗體的寬和高。我們沒(méi)有父窗口,也不想要菜單,這些參數(shù)被設(shè)為NULL。還傳遞了窗口的實(shí)例,最后一個(gè)參數(shù)被設(shè)為NULL。
注意我們?cè)诖绑w風(fēng)格中包括了WS_CLIPSIBLINGS和WS_CLIPCHILDREN。要讓OpenGL正常運(yùn)行,這兩個(gè)屬性是必須的。他們阻止別的窗體在我們的窗體內(nèi)/上繪圖。
if (!(hWnd=CreateWindowEx( dwExStyle, // 擴(kuò)展窗體風(fēng)格
"OpenGL", // 類名字
title, // 窗口標(biāo)題
WS_CLIPSIBLINGS | // 必須的窗體風(fēng)格屬性
WS_CLIPCHILDREN | // 必須的窗體風(fēng)格屬性
dwStyle, // 選擇的窗體屬性
0,0,
// 窗口位置
WindowRect.right-WindowRect.left, // 計(jì)算調(diào)整好的窗口寬度
WindowRect.bottom-WindowRect.top, // 計(jì)算調(diào)整好的窗口高度
NULL, // 無(wú)父窗口
NULL, // 無(wú)菜單
hInstance, // 實(shí)例
NULL))) // 不向WM_CREATE傳遞任何東東
下來(lái)我們檢查看窗口是否正常創(chuàng)建。如果成功,
hWnd保存窗口的句柄。如果失敗,彈出消息窗口,并退出程序。
{
KillGLWindow(); // 重置顯示區(qū)
MessageBox(NULL,"Window Creation Error.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回 FALSE
}
下面的代碼描述象素格式。我們選擇了通過(guò)RGBA(紅、綠、藍(lán)、alpha通道)支持OpenGL和雙緩存的格式。我們?cè)噲D找到匹配我們選定的色彩深度(16位、24位、32位)的象素格式。最后設(shè)置16位Z-緩存。其余的參數(shù)要么未使用要么不重要(
stencil buffer:模板緩存與
accumulation buffer:聚集緩存除外)。
static PIXELFORMATDEscrīptOR pfd= //pfd 告訴窗口我們所希望的東東
{ sizeof(PIXELFORMATDEscrīptOR), //上訴格式描述符的大小
1, // 版本號(hào)
PFD_DRAW_TO_WINDOW | // 格式必須支持窗口
PFD_SUPPORT_OPENGL | // 格式必須支持OpenGL
PFD_DOUBLEBUFFER, // 必須支持雙緩沖
PFD_TYPE_RGBA, // 申請(qǐng) RGBA 格式
bits, // 選定色彩深度
0, 0, 0, 0, 0, 0, // 忽略的色彩位
0, // 無(wú)Alpha緩存
0, // 忽略Shift Bit
0, // 無(wú)聚集緩存
0, 0, 0, 0, // 忽略聚集位
16, // 16位 Z-緩存 (深度緩存)
0, // 無(wú)模板緩存
0, // 無(wú)輔助緩存
PFD_MAIN_PLANE, // 主繪圖層
0, // 保留
0, 0, 0 // 忽略層遮罩
}; 如果前面創(chuàng)建窗口時(shí)沒(méi)有錯(cuò)誤發(fā)生,我們接著嘗試取得OpenGL設(shè)備描述表。若無(wú)法取得DC,彈出錯(cuò)誤消息程序退出(返回FALSE)。
if (!(hDC=GetDC(hWnd))) // 取得設(shè)備描述表了么? {
KillGLWindow(); // 重置顯示區(qū)
MessageBox(NULL,"Can’t Create A GL Device Context.",
"ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回 FALSE
} 設(shè)法為OpenGL窗口取得設(shè)備描述表后,我們嘗試找到對(duì)應(yīng)與此前我們選定的象素格式的象素格式。如果Windows不能找到的話,彈出錯(cuò)誤消息,并退出程序(返回FALSE)。
if (!(PixelFormat=ChoosePixelFormat(hDC,&pfd))) // Windows 找到相應(yīng)的象素格式了嗎?
{
KillGLWindow(); // 重置顯示區(qū)
MessageBox(NULL,"Can’t Find A Suitable PixelFormat.",
"ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回 FALSE
} Windows找到相應(yīng)的象素格式后,嘗試設(shè)置象素格式。如果無(wú)法設(shè)置,彈出錯(cuò)誤消息,并退出程序(返回FALSE)。
if(!SetPixelFormat(hDC,PixelFormat,&pfd)) // 能夠設(shè)置象素格式么?
{
KillGLWindow(); // 重置顯示區(qū)
MessageBox(NULL,"Can’t Set The PixelFormat.","ERROR",MB_OK|MB_ICONEXCLAMATION); return FALSE; // 返回 FALSE
} 正常設(shè)置象素格式后,嘗試取得著色描述表。如果不能取得著色描述表的話,彈出錯(cuò)誤消息,并退出程序(返回FALSE)。
if (!(hRC=wglCreateContext(hDC))) // 能否取得著色描述表?
{
KillGLWindow(); // 重置顯示區(qū)
MessageBox(NULL,"Can’t Create A GL Rendering Context.",
"ERROR",MB_OK|MB_ICONEXCLAMATION); return FALSE; // 返回 FALSE
} 如果到現(xiàn)在仍未出現(xiàn)錯(cuò)誤的話,我們已經(jīng)設(shè)法取得了設(shè)備描述表和著色描述表。接著要做的是激活著色描述表。如果無(wú)法激活,彈出錯(cuò)誤消息,并退出程序(返回FALSE)。
if(!wglMakeCurrent(hDC,hRC)) // 嘗試激活著色描述表
{
KillGLWindow(); // 重置顯示區(qū)
MessageBox(NULL,"Can’t Activate The GL Rendering Context.",
"ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回 FALSE
}
一切順利的話,OpenGL窗口已經(jīng)創(chuàng)建完成,接著可以顯示它啦。將它設(shè)為前端窗口(給它更高的優(yōu)先級(jí)),并將焦點(diǎn)移至此窗口。然后調(diào)用ReSizeGLScene 將屏幕的寬度和高度設(shè)置給透視OpenGL屏幕。
ShowWindow(hWnd,SW_SHOW); // 顯示窗口
SetForegroundWindow(hWnd); // 略略提高優(yōu)先級(jí)
SetFocus(hWnd); // 設(shè)置鍵盤的焦點(diǎn)至此窗口
ReSizeGLScene(width, height); // 設(shè)置透視 GL 屏幕
跳轉(zhuǎn)至InitGL(),這里可以設(shè)置光照、紋理、等等任何需要設(shè)置的東東。您可以在InitGL()內(nèi)部自行定義錯(cuò)誤檢查,并返回TRUE(一切正常)或FALSE(有什么不對(duì))。例如,如果您在InitGL()內(nèi)裝載紋理并出現(xiàn)錯(cuò)誤,您可能希望程序停止。如果您返回FALSE的話,下面的代碼會(huì)彈出錯(cuò)誤消息,并退出程序。
if (!InitGL()) // 初始化新建的GL窗口
{
KillGLWindow(); // 重置顯示區(qū)
MessageBox(NULL,"Initialization Failed.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回 FALSE
}
到這里可以安全的推定創(chuàng)建窗口已經(jīng)成功了。我們向WinMain()返回TRUE,告知WinMain()沒(méi)有錯(cuò)誤,以防止程序退出。
return TRUE; // 成功
}
下面的代碼處理所有的窗口消息。當(dāng)我們注冊(cè)好窗口類之后,程序跳轉(zhuǎn)到這部分代碼處理窗口消息。
LRESULT CALLBACK WndProc( HWND hWnd, // 窗口的句柄
UINT uMsg, // 窗口的消息
WPARAM wParam, // 附加的消息內(nèi)容
LPARAM lParam) // 附加的消息內(nèi)容
{
下來(lái)的代碼比對(duì)
uMsg的值,然后轉(zhuǎn)入case處理,
uMsg中保存了我們要處理的消息名字。
switch (uMsg) // 檢查Windows消息 { 如果
uMsg等于WM_ACTIVE,查看窗口是否仍然處于激活狀態(tài)。如果窗口已被最小化,將變量
active設(shè)為FALSE。如果窗口已被激活,變量
active的值為TRUE。
case WM_ACTIVATE: // 監(jiān)視窗口激活消息
{
if (!HIWORD(wParam)) // 檢查最小化狀態(tài)
{
active=TRUE; // 程序處于激活狀態(tài)
}
else
{
active=FALSE; // 程序不再激活
}
return 0; // 返回消息循環(huán)
} 如果消息是WM_SYSCOMMAND(系統(tǒng)命令),再次比對(duì)
wParam。如果
wParam是SC_SCREENSAVE或SC_MONITORPOWER的話,不是有屏幕保護(hù)要運(yùn)行,就是顯示器想進(jìn)入節(jié)電模式。返回0可以阻止這兩件事發(fā)生。
case WM_SYSCOMMAND: // 中斷系統(tǒng)命令I(lǐng)ntercept System Commands
{
switch (wParam) // 檢查系統(tǒng)調(diào)用Check System Calls
{
case SC_SCREENSAVE: // 屏保要運(yùn)行?
case SC_MONITORPOWER: // 顯示器要進(jìn)入節(jié)電模式?
return 0; // 阻止發(fā)生
}
break; // 退出
} 如果
uMsg是WM_CLOSE,窗口將被關(guān)閉。我們發(fā)出退出消息,主循環(huán)將被中斷。變量
done被設(shè)為TRUE,WinMain()的主循環(huán)中止,程序關(guān)閉。
case WM_CLOSE: // 收到Close消息?
{
PostQuitMessage(0); // 發(fā)出退出消息
return 0;
} 如果鍵盤有鍵按下,通過(guò)讀取
wParam的信息可以找出鍵值。我將鍵盤數(shù)組
keys[ ]相應(yīng)的數(shù)組組成員的值設(shè)為TRUE。這樣以后就可以查找key[ ]來(lái)得知什么鍵被按下。允許同時(shí)按下多個(gè)鍵。
case WM_KEYDOWN: // 有鍵按下么?
{
keys[wParam] = TRUE; // 如果是,設(shè)為TRUE
return 0; // 返回
} 同樣,如果鍵盤有鍵釋放,通過(guò)讀取
wParam的信息可以找出鍵值。然后將鍵盤數(shù)組
keys[ ]相應(yīng)的數(shù)組組成員的值設(shè)為FALSE。這樣查找key[ ]來(lái)得知什么鍵被按下,什么鍵被釋放了。鍵盤上的每個(gè)鍵都可以用0~255之間的一個(gè)數(shù)來(lái)代表。舉例來(lái)說(shuō),當(dāng)我們按下40所代表的鍵時(shí),
keys[40]的值將被設(shè)為TRUE。放開(kāi)的話,它就被設(shè)為FALSE。這也是key數(shù)組的原理。
case WM_KEYUP: // 有鍵放開(kāi)么?
{
keys[wParam] = FALSE; // 如果是,設(shè)為FALSE
return 0; // 返回
} 當(dāng)調(diào)整窗口時(shí),
uMsg最后等于消息
WM_SIZE。讀取lParam的LOWORD和HIWORD可以得到窗口新的寬度和高度。將他們傳遞給ReSizeGLScene(),OpenGL場(chǎng)景將調(diào)整為新的寬度和高度。
case WM_SIZE:
{
ReSizeGLScene(LOWORD(lParam),HIWORD(lParam));
return 0;
}
}
其余無(wú)關(guān)的消息被傳遞給DefWindowProc,讓W(xué)indows自行處理。
// 向 DefWindowProc傳遞所有未處理的消息。 return DefWindowProc(hWnd,uMsg,wParam,lParam);
} 下面是我們的Windows程序的入口。將會(huì)調(diào)用窗口創(chuàng)建例程,處理窗口消息,并監(jiān)視人機(jī)交互。
int WINAPI WinMain( HINSTANCE hInstance, // 實(shí)例
HINSTANCE hPrevInstance, // 前一個(gè)實(shí)例
LPSTR lpCmdLine, // 命令行參數(shù)
int nCmdShow) // 窗口顯示狀態(tài)
{ 我們?cè)O(shè)置兩個(gè)變量。
msg用來(lái)檢查是否有消息等待處理。
done的初始值設(shè)為FALSE。這意味著我們的程序仍未完成運(yùn)行。只要程序
done保持FALSE,程序繼續(xù)運(yùn)行。一旦
done的值改變?yōu)門RUE,程序退出。
MSG msg; // Windowsx消息結(jié)構(gòu) BOOL done=FALSE; // 用來(lái)退出循環(huán)的Bool 變量 這段代碼完全可選。程序彈出一個(gè)消息窗口,詢問(wèn)用戶是否希望在全屏模式下運(yùn)行。如果用戶單擊NO按鈕,
fullscreen變量從缺省的TRUE改變?yōu)镕ALSE,程序也改在窗口模式下運(yùn)行。
// 提示用戶選擇運(yùn)行模式
if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?",
"Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)
{
fullscreen=FALSE;// 窗口模式 } 接著創(chuàng)建OpenGL窗口。CreateGLWindow函數(shù)的參數(shù)依次為標(biāo)題、寬度、高度、色彩深度,以及全屏標(biāo)志。就這么簡(jiǎn)單。我很欣賞這段代碼的簡(jiǎn)潔。如果未能創(chuàng)建成功,函數(shù)返回FALSE。程序立即退出。
// 創(chuàng)建OpenGL窗口 if (!CreateGLWindow("NeHe’s OpenGL Framework",640,480,16,fullscreen))
{
return 0; // 失敗退出 } 下面是循環(huán)的開(kāi)始。只要
done保持FALSE,循環(huán)一直進(jìn)行。
while(!done) // 保持循環(huán)直到 done=TRUE { 我們要做的第一件事是檢查是否有消息在等待。使用PeekMessage()可以在不鎖住我們的程序的前提下對(duì)消息進(jìn)行檢查。許多程序使用GetMessage(),也可以很好的工作