跳至內容

OpenGL 程式設計/GLStart/Tut2

來自華夏公益教科書,開放的書籍,用於開放的世界

教程 2:在 Windows 上設定 OpenGL

[編輯 | 編輯原始碼]

使用第一課中的程式碼(Win32 入門),我們將設定 Windows 程式以使用 OpenGL。首先,啟動我們在第一課中使用的 Dev - C++ 專案。

連結 OpenGL 庫

[編輯 | 編輯原始碼]

開啟 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。編譯並執行程式,以獲得以下輸出。

華夏公益教科書