OpenGL 程式設計/GLStart/Tut2
使用第一課中的程式碼(Win32 入門),我們將設定 Windows 程式以使用 OpenGL。首先,啟動我們在第一課中使用的 Dev - C++ 專案。
開啟 Windows 專案後,轉到頂部選單欄,點選“專案”,然後點選“專案選項”。在“專案選項”視窗中,點選“引數”選項卡。在第三個視窗(“連結器”視窗)中,點選“新增庫或物件”按鈕。從那裡導航到您安裝 Dev - C++ 的位置(很可能是“C:/Dev-Cpp”)。到達那裡後,開啟“lib”資料夾。在那裡點選 libglu32.a 檔案,然後按住“Control”鍵並點選 libopengl32.a 檔案來選擇它們。然後點選對話方塊底部的“開啟”按鈕。然後點選“確定”按鈕。
現在,回到您的原始碼,在包含 Windows 標頭檔案下方,新增以下包含標頭檔案
#include <gl/gl.h> #include <gl/glu.h>
這將包含適當的 OpenGL 和 GLU 標頭檔案。
現在我們需要建立兩個名為 Contexts 的變數。上下文是一個執行某些程序的結構。我們將要處理的兩個上下文是裝置上下文和渲染上下文。裝置上下文是 Windows 特定上下文,用於基本繪圖,例如線條、形狀等... 裝置上下文只能繪製二維物件。另一方面,渲染上下文是 OpenGL 特定上下文,用於在三維空間中繪製物件。裝置上下文用 HDC 宣告,渲染上下文用 HGLRC 宣告。像這樣在包含標頭檔案下方宣告這兩個上下文變數
HDC hDC; //device context HGLRC hglrc; //rendering context
我們現在將初始化這兩個上下文,以便我們最終可以繪製一些與 OpenGL 相關的內容。
在程式碼中,轉到訊息過程 (WinProc)。現在我們只有一個訊息 (WM_DESTROY)。我們想要做的是在程式首次開啟時建立上下文。用於此的 Windows 訊息是 WM_CREATE,它在視窗首次開啟時被處理
case WM_CREATE:
在該訊息下,我們必須檢索當前裝置上下文。為此,我們將常規裝置上下文 (hDC) 設定為 GetDC() 函式,該函式將視窗控制代碼作為引數(我們在 WinProc 宣告中宣告的 hWnd)。此函式返回當前裝置上下文
hDC = GetDC(hWnd);
現在我們將保留此訊息。我們稍後會回到這個訊息。現在我們需要做的是設定所謂的程式的畫素格式。
畫素格式是繪製內容時畫素在視窗上的顯示方式。儲存畫素資料的結構稱為 PIXELFORMATDESCRIPTOR
typedef struct tagPIXELFORMATDESCRIPTOR { // pfd
WORD nSize;
WORD nVersion;
DWORD dwFlags;
BYTE iPixelType;
BYTE cColorBits;
BYTE cRedBits;
BYTE cRedShift;
BYTE cGreenBits;
BYTE cGreenShift;
BYTE cBlueBits;
BYTE cBlueShift;
BYTE cAlphaBits;
BYTE cAlphaShift;
BYTE cAccumBits;
BYTE cAccumRedBits;
BYTE cAccumGreenBits;
BYTE cAccumBlueBits;
BYTE cAccumAlphaBits;
BYTE cDepthBits;
BYTE cStencilBits;
BYTE cAuxBuffers;
BYTE iLayerType;
BYTE bReserved;
DWORD dwLayerMask;
DWORD dwVisibleMask;
DWORD dwDamageMask;
} PIXELFORMATDESCRIPTOR;
這裡有很多欄位。好訊息是我們只需要填寫幾個欄位就可以使該結構工作。讓我們開始在一個新函式中設定畫素格式。
在程式碼的頂部,新增以下函式呼叫
void SetupPixels(HDC hDC) {
之所以將裝置上下文作為引數,是因為當我們將畫素格式設定為與視窗一起工作時,我們需要將視窗的裝置上下文作為引數傳遞給其中一個函式。
現在,在剛剛建立的函式中,我們宣告一個型別為 Integer 的變數,名為“pixelFormat”。此變數將儲存一個索引,該索引引用我們要建立的畫素格式。之後,宣告一個型別為 PIXELFORMATDESCRIPTOR 的變數,名為“pfd”,以儲存實際的畫素格式資料
int pixelFormat;
PIXELFORMATDESCRIPTOR pfd;
現在讓我們開始填充畫素格式的幾個欄位。
我們填充的第一個欄位是 nSize 欄位,它設定為結構本身的大小
pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
我們將填充的下一個欄位是名為 dwFlags 的標誌欄位。這些設定了畫素格式的某些屬性。我們將為此設定三個標誌。第一個 PFD_SUPPORT_OPENGL 允許畫素格式能夠在 OpenGL 中繪製。下一個 PFD_DRAW_TO_WINDOW 告訴畫素格式將所有內容繪製到我們提供的視窗上。最後一個 PFD_DOUBLEBUFFER 允許我們透過提供兩個緩衝區來繪製來建立平滑動畫,這兩個緩衝區會被切換以使動畫平滑
pfd.dwFlags = PFD_SUPPORT_OPENGL | PFD_DRAW_TO_WINDOW | PFD_DOUBLEBUFFER;
我們將填充的下一個欄位是版本欄位 nVersion,它始終設定為 1
pfd.nVersion = 1;
下一個欄位將是畫素型別 iPixelType,它設定我們想要支援的顏色型別。為此,我們將使用 PFD_TYPE_RGBA 以便獲得紅色、綠色、藍色和 alpha 顏色集(暫時不用擔心 alpha 部分。在需要時,我們會進行詳細說明)
pfd.iPixelType = PFD_TYPE_RGBA;
下一個欄位 cColorBits 指定要使用的顏色位數。我們將將其設定為 32
pfd.cColorBits = 32;
我們設定的最後一個欄位 cDepthBits 設定深度緩衝區位。現在將其設定為 24
pfd.cDepthBits = 24;
設定完畫素格式的欄位後,我們需要將裝置上下文設定為與新建立的畫素格式匹配。為此,我們使用 ChoosePixelFormat() 函式,該函式將裝置上下文作為第一個引數,並將我們之前建立的 PIXELFORMATDESCRIPTOR 結構的地址作為第二個引數 (pfd)。我們將我們在函式開頭宣告的整型變數 (pixelFormat) 設定為該函式的返回值
pixelFormat = ChoosePixelFormat(hDC, &pfd);
現在,我們使用 SetPixelFormat() 函式將畫素格式設定為裝置上下文,該函式將裝置上下文作為三個引數,畫素格式整數和 PIXELFORMATDESCRIPTOR 結構的地址。此外,我們將檢查該函式是否正常工作。此特定函式根據它是否成功返回一個布林值。我們將檢查它是否成功。如果它沒有成功,那麼我們將使用訊息框提醒使用者並關閉程式
if(!SetPixelFormat(hDC, pixelFormat, &pfd))
{
MessageBox(NULL,"Error setting up Pixel Format","ERROR",MB_OK);
PostQuitMessage(0);
}
現在我們結束函式,因為我們已經完成了設定畫素
}
現在,我們回到視窗過程中的 WM_CREATE 訊息,並在獲取裝置上下文 (hDC = GetDC(hWnd)) 下方,我們呼叫 SetupPixels() 函式並將裝置上下文作為引數傳遞
SetupPixels(hDC);
現在,請記住我們之前宣告的渲染上下文 (hglrc)。好吧,我們現在將使用 wglCreateContext() 函式建立它,該函式將常規裝置上下文作為引數
hglrc = wglCreateContext(hDC);
現在,我們將使渲染上下文成為我們在整個程式中使用的當前上下文。為此,我們使用 wglMakeCurrent() 函式,該函式將裝置上下文作為第一個引數,將渲染上下文作為第二個引數
wglMakeCurrent(hDC, hglrc);
現在我們完成了 WM_CREATE 訊息。確保在訊息末尾包含 break 語句
break;
還記得我們在第一課中建立的 WM_DESTROY 訊息,它負責程式退出嗎?我們需要在那裡釋放渲染上下文,以避免記憶體洩漏。因此,回到 WM_DESTROY 訊息,我們將新增程式碼。
首先,在實際刪除渲染上下文之前,我們必須確保它不再處於活動狀態。為此,我們再次使用 wglMakeCurrent() 函式。如果還記得的話,此函式將裝置上下文和渲染上下文作為引數。為此,我們傳遞裝置上下文,但對於渲染上下文,我們輸入 NULL 來指示我們不希望渲染上下文處於活動狀態
wglMakeCurrent(hDC,NULL);
現在我們可以安全地釋放渲染上下文。為此,我們使用 wglDeleteContext() 函式,該函式將要刪除的渲染上下文作為單個引數
wglDeleteContext(hglrc);
確保在這兩個函式呼叫之後,您仍然擁有 PostQuitMessage() 和 break 語句,與往常一樣。以下是整個 WM_DESTROY 訊息的定義
case WM_DESTROY:
wglMakeCurrent(hDC,NULL);
wglDeleteContext(hglrc);
PostQuitMessage(0);
break;
大小調整發生在有人擴充套件視窗的寬度和/或高度時。如果我們不控制這一點,OpenGL 會感到困惑並開始錯誤地繪製內容。因此,首先我們將建立一個名為 Resize() 的函式,該函式將處理視窗的大小調整。此函式將視窗的寬度和高度作為兩個引數,我將在後面討論如何接收這些引數
void Resize(int width, int height)
{
在此函式中,我們必須做的第一件事是設定視口。視口是我們希望看到 OpenGL 繪製進行的視窗部分。設定視口的函式稱為 glViewport()
void glViewport(
GLint x,
GLint y,
GLsizei width,
GLsizei height
);
第一個和第二個引數,x 和 y,是視口左下角的座標。由於我們想要在整個視窗上看到繪製的內容,我們將這兩個引數都設定為 0,表示視窗的左下角。第三個和第四個引數,width 和 height,是視口的寬度和高度(以畫素為單位)。由於我們希望它覆蓋整個視窗,因此將其設定為傳遞給 Resize() 函式的寬度和高度引數。為了安全起見,請確保將第三個和第四個引數轉換為 GLsizei 資料型別。以下是包含引數的 glViewport() 函式:
glViewport(0,0,(GLsizei)width,(GLsizei)height);
現在我們已經設定了視口,需要設定所謂的投影。投影基本上是使用者如何看待所有東西。投影有兩種型別:正投影和透視投影。正投影是一種不切實際的檢視。為了更好地解釋它,當在正投影 3D 場景中繪製物體時,放置在遠離另一個物體的物體看起來大小相同,即使考慮了距離。另一方面,透視投影更逼真,例如,遠離觀看者的物體看起來比靠近觀看者的物體更小。現在你對投影有了更好的瞭解,讓我們在程式碼中建立一個投影。在本課中,我們將使用透視投影。
要開始編輯投影,我們需要選擇投影矩陣。為此,我們使用 glMatrixMode() 函式,它接受一個引數,即我們要編輯的矩陣。要編輯投影矩陣,我們給函式提供值 GL_PROJECTION。
glMatrixMode(GL_PROJECTION);
在我們開始編輯投影矩陣之前,我們需要確保當前矩陣是單位矩陣。為此,我們呼叫 glLoadIdentity() 函式,該函式不接受任何引數,只是將單位矩陣載入為當前矩陣。
glLoadIdentity();
要設定透視投影,我們使用 gluPerspective() 函式。
void gluPerspective( GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar );
第一個引數,fovy,是 y 方向的視場角(以度為單位)。你可以將其設定為 45 以獲得正常的視角。第二個引數,aspect,是 x 方向的視場。這通常由寬度與高度的比例決定。第三個和第四個引數,zNear 和 zFar,是觀看者可以看到的深度距離。我們將 zNear 設定為 1.0,將 zFar 設定為 1000.0,以便使用者可以獲得很大的深度檢視。
以下是包含縱橫比約束選項和所有主要引數的函式:
gluPerspective(recalculatefovy(),(GLfloat)width/(GLfloat)height,1.0f,1000.0f);
float recalculatefovy()
{
return std::atan(std::tan(45.0f * 3.14159265358979f / 360.0f) / aspectaxis()) * 360.0f / 3.14159265358979f;
}
float aspectaxis()
{
GLFloat outputzoom = 1.0f;
GLFloat aspectorigin = 16.0f / 9.0f;
GLInt aspectconstraint = 1; /* Sets the aspect axis constraint to maintain the FOV direction when resizing; the first constraint is conditional and maintains horizontal space if below a specific ratio and the other extends vertical space. Default is 0. */
switch (aspectconstraint)
{
case 1:
if (((GLfloat)width / (GLfloat)height) < aspectorigin)
{
outputzoom *= ((GLfloat)width / (GLfloat)height / aspectorigin)
}
else
{
outputzoom *= (aspectorigin / aspectorigin)
}
break;
case 2:
outputzoom *= ((GLfloat)width / (GLfloat)height / aspectorigin)
break;
default:
outputzoom *= (aspectorigin / aspectorigin)
}
return outputzoom;
}
現在,我們必須將矩陣模式切換到模型檢視矩陣。再呼叫一次 glMatrixMode() 函式,但這次引數為 GL_MODELVIEW。模型檢視矩陣包含我們將繪製的物件資訊。我將在後面的課程中詳細介紹它。
glMatrixMode(GL_MODELVIEW);
現在,我們需要透過呼叫 glLoadIdentity() 函式來重置模型檢視矩陣。呼叫完該函式後,我們就完成了 Resize() 函式的編寫。
glLoadIdentity(); }
現在,我們必須將此 Resize() 函式呼叫放到視窗過程中。我們將放入其中的訊息稱為 WM_SIZE,它在使用者調整視窗大小時被呼叫。
case WM_SIZE:
現在,我們需要一種方法來跟蹤當前視窗的寬度和高度。首先,在訊息的 switch 結構之前,宣告兩個名為“w”(寬度)和“h”(高度)的整數變數。
int w,h;
switch(msg)
回到 WM_SIZE 訊息,我們需要將剛剛建立的變數設定為當前寬度和高度。為此,我們使用傳遞給視窗過程函式的 lParam 引數。要獲取視窗的寬度,可以使用 LOWORD() 宏函式,並將 lParam 變數作為單個引數傳入。它將返回視窗的當前寬度。要獲取視窗的當前高度,可以使用 HIWORD() 宏函式,它將返回視窗的當前高度。最後,將兩個整數變數 (w,h) 傳遞給我們建立的 Resize() 函式,這樣就完成了 WM_SIZE 訊息的處理。
case WM_SIZE:
w = LOWORD(lParam);
h = HIWORD(lParam);
Resize(w,h);
break;
用 OpenGL 繪製內容
[edit | edit source]現在我們已經用程式設定了 OpenGL,讓我們測試一下,以確保它設定正確。
首先建立一個名為 Render() 的新函式。此函式將負責本程式中執行的所有 OpenGL 繪製操作。
void Render()
{
我們在此函式中做的第一件事叫做緩衝區清除。我將在後面的課程中討論緩衝區,但請確保在你渲染任何內容到螢幕之前清除你正在使用的緩衝區。為此,我們使用 glClear() 函式,它接受我們要清除的緩衝區作為引數。我們將傳入 GL_COLOR_BUFFER_BIT 用於顏色緩衝區,以及 GL_DEPTH_BUFFER_BIT 用於深度緩衝區。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
現在,我們必須使用 glLoadIdentity() 函式載入單位矩陣,以便從原點重新開始。
glLoadIdentity();
到目前為止,我們的檢視以原點為中心,整個視窗的寬度和高度均為 1 個單位。一個單位是 OpenGL 使用的一種測量單位。這基本上是一個使用者定義的測量系統。預設的視窗寬度和高度現在是 1 個單位。要獲得更多視窗單位,我們需要沿負 z 軸移動,即遠離觀看者移動。我們只需要沿 -z 方向移動 4 個單位,這樣視窗的寬度和高度就都是 4 個單位。為此,我們使用 glTranslatef() 函式。
void glTranslatef(
GLfloat x,
GLfloat y,
GLfloat z
);
引數 (x,y,z) 指定要沿哪個軸移動。由於我們想要沿 z 軸負向移動 4 個單位,因此我們將前兩個引數保留為 0.0,並將 -4.0f 放入 z 引數。
glTranslatef(0.0f,0.0f,-4.0f);
現在我們終於要在螢幕上繪製內容了。首先,當你繪製螢幕上的內容時,如果沒有指定要繪製物件的顏色,那麼 OpenGL 會自動將物件的顏色設定為白色。要解決這個問題,在繪製任何物件之前,使用 glColor3f() 函式,該函式接受三個引數,分別是紅色、綠色和藍色的顏色值。你需要知道的一件事是,你可以輸入的顏色值範圍是 0.0 到 1.0,而不是通常的 RGB 值,其範圍是 0 到 255。現在,我們將要繪製的物件顏色設定為藍色,方法是將紅色和綠色值設定為 0.0,藍色值設定為 1.0f。
glColor3f(0.0f,0.0f,1.0f);
現在,我們終於要進入實際繪製內容的部分了。OpenGL 繪製的工作原理是,首先你需要指定要繪製的物件型別。之後,你需要指定物件的頂點。
要開始繪製物件,我們需要使用 glBegin() 函式,它接受一個引數,即我們要繪製的物件型別。glBegin() 函式告訴 OpenGL,此函式呼叫之後的語句將用於繪製特定的內容。我們現在將為此函式提供的引數是 GL_POLYGON,它告訴 OpenGL 我們將要繪製一個多邊形。
glBegin(GL_POLYGON);
現在,我們需要指定我們想要連線起來形成多邊形的頂點。對於這個例子,我們要繪製的是一個正方形,所以我們只需要指定 4 個頂點即可。要繪製一個頂點,我們使用 glVertex3f() 函式,該函式接受三個引數,分別是頂點的 x、y 和 z 位置。OpenGL 視窗最初的寬度和高度為 1 個單位。我們在 Render() 函式的前面部分使用了 glTranslatef() 函式向後移動了 4 個單位。所以這意味著檢視視窗的寬度和高度現在都是 4 個單位。原點從視窗的完全中心開始,並且就像一個標準座標系一樣。我們將繪製第一個頂點靠近右上角,座標為 (1.0f,1.0f,0.0f),這意味著我們將頂點放置到右邊 1 個單位,向上 1 個單位。
glVertex3f(1.0f,1.0f,0.0f);
現在,我們將設定正方形的其他四個角,就像我們在第一個頂點上所做的那樣。
glVertex3f(-1.0f,1.0f,0.0f);
glVertex3f(-1.0f,-1.0f,0.0f);
glVertex3f(1.0f,-1.0f,0.0f);
現在,要結束繪製,我們使用 glEnd() 函式告訴 OpenGL 我們已經完成了繪製。這也完成了我們的 Render() 函式。
glEnd(); }
控制渲染迴圈
[edit | edit source]現在,我們必須回到 WinMain() 函式,並將 Render() 函式放在某個位置,以便它在迴圈中被呼叫。首先,在 WinMain() 中的 UpdateWindow() 函式呼叫下方,我們需要確保我們有當前的裝置上下文,為此,我們使用之前使用過的 GetDC() 函式。
hDC = GetDC(hWnd);
我們進入渲染迴圈之前還要做的一件事是將螢幕清除為某種顏色。為此,我們使用 glClearColor() 函式,該函式接受 4 個引數,分別是紅色、綠色、藍色和 alpha 顏色值。將這些值都設定為 0,並將該函式放在之前的 GetDC() 函式呼叫下方。
glClearColor(0.0f,0.0f,0.0f,0.0f);
現在,我們將 Render() 函式放在 WinMain() 末尾的 WHILE 迴圈中。在程式碼 while(1) 下方,放入 Render() 函式。
while(1)
{
Render();
我們必須在編譯此程式之前做的最後一件事是交換緩衝區。由於我們將畫素格式設定為雙緩衝,因此我們使用 SwapBuffers() 函式,該函式接受一個裝置上下文作為單個引數。將此函式呼叫放在 Render() 函式呼叫下方。
SwapBuffers(hDC);
現在,我們完成了用 Windows 設定 OpenGL。編譯並執行程式,以獲得以下輸出。

