在 Windows 上手動設定 OpenGL

最後包含完整的示例程式碼

OpenGL 的 Windows 元件

WGL

WGL(可以發音為 wiggle )代表“Windows-GL”,如“Windows 和 OpenGL 之間的介面” - 來自 Windows API 的一組函式,用於與 OpenGL 通訊。WGL 函式具有 wgl 字首,其標記具有 WGL_ 字首。

Microsoft 系統支援的預設 OpenGL 版本為 1.1。這是一個非常舊的版本(最近的版本是 4.5)。獲取最新版本的方法是更新圖形驅動程式,但你的圖形卡必須支援這些新版本。

可以在此處找到 WGL 功能的完整列表。

圖形裝置介面(GDI)

GDI(今天更新為 GDI +)是一個 2D 繪圖介面,允許你在 Windows 中繪製視窗。你需要 GDI 來初始化 OpenGL 並允許它與它互動(但實際上不會使用 GDI 本身)。

在 GDI 中,每個視窗都有一個裝置上下文(DC) ,用於在呼叫函式時標識繪圖目標(將其作為引數傳遞)。但是,OpenGL 使用自己的渲染上下文(RC) 。因此,DC 將用於建立 RC。

基本設定

建立一個視窗

因此,對於 OpenGL 中的操作,我們需要 RC,要獲得 RC,我們需要 DC,要獲得 DC,我們需要一個視窗。使用 Windows API 建立視窗需要幾個步驟。這是一個基本例程,因此有關更詳細的說明,你應該參考其他文件,因為這不是關於使用 Windows API。

這是 Windows 設定,因此必須包含 Windows.h,程式的入口點必須是 WinMain 過程及其引數。該程式還需要連結到 opengl32.dll 和 gdi32.dll(無論你使用的是 64 位還是 32 位系統)。

首先,我們需要使用 WNDCLASS 結構來描述我們的視窗。它包含有關我們要建立的視窗的資訊:

/* REGISTER WINDOW */
WNDCLASS window_class;

// Clear all structure fields to zero first
ZeroMemory(&window_class, sizeof(window_class));

// Define fields we need (others will be zero)
window_class.style = CS_OWNDC;
window_class.lpfnWndProc = window_procedure; // To be introduced later
window_class.hInstance = instance_handle;
window_class.lpszClassName = TEXT("OPENGL_WINDOW");

// Give our class to Windows
RegisterClass(&window_class);
/* *************** */

有關每個欄位含義的精確解釋(以及完整的欄位列表),請參閱 MSDN 文件。

然後,我們可以使用 CreateWindowEx 建立一個視窗。建立視窗後,我們可以獲得其 DC:

/* CREATE WINDOW */
HWND window_handle = CreateWindowEx(WS_EX_OVERLAPPEDWINDOW,
                                    TEXT("OPENGL_WINDOW"),
                                    TEXT("OpenGL window"),
                                    WS_OVERLAPPEDWINDOW,
                                    0, 0,
                                    800, 600,
                                    NULL,
                                    NULL,
                                    instance_handle,
                                    NULL);

HDC dc = GetDC(window_handle);

ShowWindow(window_handle, SW_SHOW);
/* ************* */

最後,我們需要建立一個從 OS 接收視窗事件的訊息迴圈:

/* EVENT PUMP */
MSG msg;

while (true) {
    if (PeekMessage(&msg, window_handle, 0, 0, PM_REMOVE)) {
        if (msg.message == WM_QUIT)
            break;
        
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    
    // draw(); <- there goes your drawing

    SwapBuffers(dc); // To be mentioned later
}
/* ********** */

畫素格式

OpenGL 需要了解有關視窗的一些資訊,例如顏色位,緩衝方法等。為此,我們使用畫素格式。但是,我們只能向作業系統建議我們需要什麼樣的畫素格式,作業系統將提供最相似的支援,我們無法直接控制它。這就是為什麼它只被稱為描述符。

/* PIXEL FORMAT */
PIXELFORMATDESCRIPTOR descriptor;

// Clear all structure fields to zero first
ZeroMemory(&descriptor, sizeof(descriptor));

// Describe our pixel format
descriptor.nSize = sizeof(descriptor);
descriptor.nVersion = 1;
descriptor.dwFlags = PFD_DRAW_TO_WINDOW | PFD_DRAW_TO_BITMAP | PFD_SUPPORT_OPENGL | PFD_GENERIC_ACCELERATED | PFD_DOUBLEBUFFER | PFD_SWAP_LAYER_BUFFERS;
descriptor.iPixelType = PFD_TYPE_RGBA;
descriptor.cColorBits = 32;
descriptor.cRedBits = 8;
descriptor.cGreenBits = 8;
descriptor.cBlueBits = 8;
descriptor.cAlphaBits = 8;
descriptor.cDepthBits = 32;
descriptor.cStencilBits = 8;

// Ask for a similar supported format and set it
int pixel_format = ChoosePixelFormat(dc, &descriptor);
SetPixelFormat(dc, pixel_format, &descriptor);
/* *********************** */

我們在 dwFlags 欄位中啟用了雙緩衝,因此我們必須呼叫 SwapBuffers 以便在繪製後檢視內容。

渲染上下文

之後,我們可以簡單地建立渲染上下文:

/* RENDERING CONTEXT */
HGLRC rc = wglCreateContext(dc);
wglMakeCurrent(dc, rc);
/* ***************** */

請注意,一次只能有一個執行緒使用 RC。如果你希望稍後從另一個執行緒使用它,你必須在那裡呼叫 wglMakeCurrent 再次啟用它(這將在它當前處於活動狀態的執行緒上停用它,依此類推)。

獲得 OpenGL 功能

通過使用函式指標獲得 OpenGL 函式。一般程式是:

  1. 以某種方式獲取函式指標型別(本質上是函式原型)
  2. 宣告我們想要使用的每個函式(使用其函式指標型別)
  3. 獲得實際功能

例如,考慮 glBegin:

// We need to somehow find something that contains something like this,
// as we can't know all the OpenGL function prototypes
typedef void (APIENTRY *PFNGLBEGINPROC)(GLenum);

// After that, we need to declare the function in order to use it
PFNGLBEGINPROC glBegin;

// And finally, we need to somehow make it an actual function

(PFN 表示指向函式的指標,然後是 OpenGL 函式的名稱,最後是 PROC - 這是通常的 OpenGL 函式指標型別名稱。)

這是在 Windows 上完成的。如前所述,微軟只發布 OpenGL 1.1。首先,可以通過包含 GL/gl.h 找到該版本的函式指標型別。在那之後,我們宣告我們打算使用的所有函式,如上所示(在標頭檔案中執行它並將它們宣告為 extern 將允許我們在載入它們之後使用它們,只需包含它)。最後,通過開啟 DLL 來載入 OpenGL 1.1 函式:

HMODULE gl_module = LoadLibrary(TEXT("opengl32.dll"));

/* Load all the functions here */
glBegin = (PFNGLBEGINPROC)GetProcAddress("glBegin");
// ...
/* *************************** */

FreeLibrary(gl_module);

但是,我們可能想要比 OpenGL 1.1 更多一點。但 Windows 並沒有為我們提供函式原型或匯出函式。原型需要從 OpenGL 登錄檔中獲取。我們感興趣的檔案有三個:GL/glext.h,GL/glcorearb.h 和 GL/wglext.h。

為了完成 Windows 提供的 GL/gl.h,我們需要 GL/glext.h。它包含(如登錄檔所述)“OpenGL 1.2 及以上相容性配置檔案和擴充套件介面”(稍後將詳細介紹配置檔案和擴充套件,我們將看到使用這兩個檔案實際上並不是一個好主意 )。

實際的功能需要通過 wglGetProcAddress 獲得(不需要為這個人開啟 DLL,它們不在那裡,只需使用該功能)。有了它,我們可以從 OpenGL 1.2 及更高版本(但不是 1.1)獲取所有功能。請注意,為了使其正常執行,必須建立 OpenGL 渲染上下文並使其成為當前。所以,例如,glClear:

// Include the header from the OpenGL registry for function pointer types

// Declare the functions, just like before
PFNGLCLEARPROC glClear;
// ...

// Get the function
glClear = (PFNGLCLEARPROC)wglGetProcAddress("glClear");

我們實際上可以構建一個使用 wglGetProcAddress 和 GetProcAddress 的包裝器 get_proc 程式:

// Get function pointer
void* get_proc(const char *proc_name)
{
    void *proc = (void*)wglGetProcAddress(proc_name);
    if (!proc) proc = (void*)GetProcAddress(gl_module, proc_name); // gl_module must be somewhere in reach

    return proc;
}

所以要結束,我們將建立一個充滿函式指標宣告的標頭檔案,如下所示:

extern PFNGLCLEARCOLORPROC glClearColor;
extern PFNGLCLEARDEPTHPROC glClearDepth;
extern PFNGLCLEARPROC glClear;
extern PFNGLCLEARBUFFERIVPROC glClearBufferiv;
extern PFNGLCLEARBUFFERFVPROC glClearBufferfv;
// And so on...

然後我們可以建立一個像 load_gl_functions 這樣的程式,我們只呼叫一次,其工作原理如下:

glClearColor = (PFNGLCLEARCOLORPROC)get_proc("glClearColor");
glClearDepth = (PFNGLCLEARDEPTHPROC)get_proc("glClearDepth");
glClear = (PFNGLCLEARPROC)get_proc("glClear");
glClearBufferiv = (PFNGLCLEARBUFFERIVPROC)get_proc("glClearBufferiv");
glClearBufferfv = (PFNGLCLEARBUFFERFVPROC)get_proc("glClearBufferfv");

你們都準備好了! 只需包含帶有函式指標和 GL 的標題。

更好的設定

OpenGL 配置檔案

OpenGL 已經開發了 20 多年,開發人員始終嚴格遵守向後相容性(BC) 。因此,新增新功能非常困難。因此,在 2008 年,它被分為兩個概況。核心和相容性。核心配置檔案打破了 BC,有利於效能改進和一些新功能。它甚至完全刪除了一些遺留功能。相容性配置檔案維護 BC 的所有版本低至 1.0,並且其中沒有一些新功能。它僅用於舊的遺留系統,所有新應用程式都應使用核心配置檔案。

因此,我們的基本設定存在問題 - 它只提供向後相容 OpenGL 1.0 的上下文。畫素格式也是有限的。有一種更好的方法,使用擴充套件。

OpenGL 擴充套件

對 OpenGL 的原始功能的任何新增都稱為擴充套件。通常,它們可以使某些事情變得合法,擴充套件引數值範圍,擴充套件 GLSL,甚至新增全新的功能。

有三個主要的擴充套件組:供應商,EXT 和 ARB。供應商擴充套件來自特定供應商,它們具有供應商特定標記,如 AMD 或 NV。EXT 擴充套件由多個供應商共同完成。一段時間後,它們可能會成為 ARB 擴充套件,這些都是官方支援的 ARB 和 ARB 批准的擴充套件。

要獲取所有擴充套件的函式指標型別和函式原型,並且如前所述,來自 OpenGL 1.2 和更高版本的所有函式指標型別,必須從 OpenGL 登錄檔下載標頭檔案。正如所討論的,對於新的應用程式,最好使用核心配置檔案,因此最好包括 GL/glcorearb.h 而不是 GL/gl.h 和 GL/glext.h(如果你使用 GL/glcorearb.h 則不包括 GL/gl.h)。

在 GL/wglext.h 中還有 WGL 的擴充套件。例如,獲取所有支援的副檔名列表的功能實際上是擴充套件本身,wglGetExtensionsStringARB(它返回一個大字串,其中包含所有支援的副檔名的空格分隔列表)。

獲取擴充套件也是通過 wglGetProcAddress 處理的,所以我們可以像以前一樣使用我們的包裝器。

高階畫素格式和上下文建立

WGL_ARB_pixel_format 擴充套件允許我們建立高階畫素格式。與以前不同,我們不使用結構。相反,我們傳遞了想要的屬性列表。

int pixel_format_arb;
UINT pixel_formats_found;

int pixel_attributes[] = {
    WGL_SUPPORT_OPENGL_ARB, 1,
    WGL_DRAW_TO_WINDOW_ARB, 1,
    WGL_DRAW_TO_BITMAP_ARB, 1,
    WGL_DOUBLE_BUFFER_ARB, 1,
    WGL_SWAP_LAYER_BUFFERS_ARB, 1,
    WGL_COLOR_BITS_ARB, 32,
    WGL_RED_BITS_ARB, 8,
    WGL_GREEN_BITS_ARB, 8,
    WGL_BLUE_BITS_ARB, 8,
    WGL_ALPHA_BITS_ARB, 8,
    WGL_DEPTH_BITS_ARB, 32,
    WGL_STENCIL_BITS_ARB, 8,
    WGL_ACCELERATION_ARB, WGL_FULL_ACCELERATION_ARB,
    WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB,
    0
};

BOOL result = wglChoosePixelFormatARB(dc, pixel_attributes, NULL, 1, &pixel_format_arb, &pixel_formats_found);

同樣,WGL_ARB_create_context 擴充套件允許我們進行高階上下文建立:

GLint context_attributes[] = {
    WGL_CONTEXT_MAJOR_VERSION_ARB, 3,
    WGL_CONTEXT_MINOR_VERSION_ARB, 3,
    WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
    0
};

HGLRC new_rc = wglCreateContextAttribsARB(dc, 0, context_attributes);

有關引數和功能的精確說明,請參閱 OpenGL 規範。

我們為什麼不從他們開始呢?好吧,那是因為擴充套件允許我們這樣做,並且為了獲得擴充套件,我們需要 wglGetProcAddress,但這隻適用於有效的有效上下文。所以實質上,在我們能夠建立我們想要的上下文之前,我們需要已經有一些上下文活動,並且它通常被稱為虛擬上下文。

但是,Windows 不允許多次設定視窗的畫素格式。因此,需要銷燬視窗並重新建立視窗以應用新的東西:

wglMakeCurrent(dc, NULL);
wglDeleteContext(rc);
ReleaseDC(window_handle, dc);
DestroyWindow(window_handle);

// Recreate the window...

完整示例程式碼:

/* We want the core profile, so we include GL/glcorearb.h. When including that, then
   GL/gl.h should not be included.

   If using compatibility profile, the GL/gl.h and GL/glext.h need to be included.

   GL/wglext.h gives WGL extensions.

   Note that Windows.h needs to be included before them. */

#include <cstdio>
#include <Windows.h>
#include <GL/glcorearb.h>
#include <GL/wglext.h>

LRESULT CALLBACK window_procedure(HWND, UINT, WPARAM, LPARAM);
void* get_proc(const char*);

/* gl_module is for opening the DLL, and the quit flag is here to prevent
   quitting when recreating the window (see the window_procedure function) */

HMODULE gl_module;
bool quit = false;

/* OpenGL function declarations. In practice, we would put these in a
   separate header file and add "extern" in front, so that we can use them
   anywhere after loading them only once. */

PFNWGLGETEXTENSIONSSTRINGARBPROC wglGetExtensionsStringARB;
PFNWGLCHOOSEPIXELFORMATARBPROC wglChoosePixelFormatARB;
PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB;
PFNGLGETSTRINGPROC glGetString;

int WINAPI WinMain(HINSTANCE instance_handle, HINSTANCE prev_instance_handle, PSTR cmd_line, int cmd_show) {
    /* REGISTER WINDOW */
    WNDCLASS window_class;

    // Clear all structure fields to zero first
    ZeroMemory(&window_class, sizeof(window_class));

    // Define fields we need (others will be zero)
    window_class.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
    window_class.lpfnWndProc = window_procedure;
    window_class.hInstance = instance_handle;
    window_class.lpszClassName = TEXT("OPENGL_WINDOW");

    // Give our class to Windows
    RegisterClass(&window_class);
    /* *************** */
        
    /* CREATE WINDOW */
    HWND window_handle = CreateWindowEx(WS_EX_OVERLAPPEDWINDOW,
                                        TEXT("OPENGL_WINDOW"),
                                        TEXT("OpenGL window"),
                                        WS_OVERLAPPEDWINDOW,
                                        0, 0,
                                        800, 600,
                                        NULL,
                                        NULL,
                                        instance_handle,
                                        NULL);
        
    HDC dc = GetDC(window_handle);
        
    ShowWindow(window_handle, SW_SHOW);
    /* ************* */
        
    /* PIXEL FORMAT */
    PIXELFORMATDESCRIPTOR descriptor;
        
    // Clear all structure fields to zero first
    ZeroMemory(&descriptor, sizeof(descriptor));
        
    // Describe our pixel format
    descriptor.nSize = sizeof(descriptor);
    descriptor.nVersion = 1;
    descriptor.dwFlags = PFD_DRAW_TO_WINDOW | PFD_DRAW_TO_BITMAP | PFD_SUPPORT_OPENGL | PFD_GENERIC_ACCELERATED | PFD_DOUBLEBUFFER | PFD_SWAP_LAYER_BUFFERS;
    descriptor.iPixelType = PFD_TYPE_RGBA;
    descriptor.cColorBits = 32;
    descriptor.cRedBits = 8;
    descriptor.cGreenBits = 8;
    descriptor.cBlueBits = 8;
    descriptor.cAlphaBits = 8;
    descriptor.cDepthBits = 32;
    descriptor.cStencilBits = 8;
        
    // Ask for a similar supported format and set it
    int pixel_format = ChoosePixelFormat(dc, &descriptor);
    SetPixelFormat(dc, pixel_format, &descriptor);
    /* *********************** */
        
    /* RENDERING CONTEXT */
    HGLRC rc = wglCreateContext(dc);
    wglMakeCurrent(dc, rc);
    /* ***************** */

    /* LOAD FUNCTIONS (should probably be put in a separate procedure) */
    gl_module = LoadLibrary(TEXT("opengl32.dll"));

    wglGetExtensionsStringARB = (PFNWGLGETEXTENSIONSSTRINGARBPROC)get_proc("wglGetExtensionsStringARB");
    wglChoosePixelFormatARB = (PFNWGLCHOOSEPIXELFORMATARBPROC)get_proc("wglChoosePixelFormatARB");
    wglCreateContextAttribsARB = (PFNWGLCREATECONTEXTATTRIBSARBPROC)get_proc("wglCreateContextAttribsARB");
    glGetString = (PFNGLGETSTRINGPROC)get_proc("glGetString");
    
    FreeLibrary(gl_module);
    /* ************** */

    /* PRINT VERSION */
    const GLubyte *version = glGetString(GL_VERSION);
    printf("%s\n", version);
    fflush(stdout);
    /* ******* */

    /* NEW PIXEL FORMAT*/
    int pixel_format_arb;
    UINT pixel_formats_found;
    
    int pixel_attributes[] = {
        WGL_SUPPORT_OPENGL_ARB, 1,
        WGL_DRAW_TO_WINDOW_ARB, 1,
        WGL_DRAW_TO_BITMAP_ARB, 1,
        WGL_DOUBLE_BUFFER_ARB, 1,
        WGL_SWAP_LAYER_BUFFERS_ARB, 1,
        WGL_COLOR_BITS_ARB, 32,
        WGL_RED_BITS_ARB, 8,
        WGL_GREEN_BITS_ARB, 8,
        WGL_BLUE_BITS_ARB, 8,
        WGL_ALPHA_BITS_ARB, 8,
        WGL_DEPTH_BITS_ARB, 32,
        WGL_STENCIL_BITS_ARB, 8,
        WGL_ACCELERATION_ARB, WGL_FULL_ACCELERATION_ARB,
        WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB,
        0
    };

    BOOL result = wglChoosePixelFormatARB(dc, pixel_attributes, NULL, 1, &pixel_format_arb, &pixel_formats_found);

    if (!result) {
        printf("Could not find pixel format\n");
        fflush(stdout);
        return 0;
    }
    /* **************** */

    /* RECREATE WINDOW */
    wglMakeCurrent(dc, NULL);
    wglDeleteContext(rc);
    ReleaseDC(window_handle, dc);
    DestroyWindow(window_handle);
    
    window_handle = CreateWindowEx(WS_EX_OVERLAPPEDWINDOW,
                                        TEXT("OPENGL_WINDOW"),
                                        TEXT("OpenGL window"),
                                        WS_OVERLAPPEDWINDOW,
                                        0, 0,
                                        800, 600,
                                        NULL,
                                        NULL,
                                        instance_handle,
                                        NULL);
        
    dc = GetDC(window_handle);
        
    ShowWindow(window_handle, SW_SHOW);
    /* *************** */

    /* NEW CONTEXT */
    GLint context_attributes[] = {
        WGL_CONTEXT_MAJOR_VERSION_ARB, 3,
        WGL_CONTEXT_MINOR_VERSION_ARB, 3,
        WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
        0
    };

    rc = wglCreateContextAttribsARB(dc, 0, context_attributes);
    wglMakeCurrent(dc, rc);
    /* *********** */
        
    /* EVENT PUMP */
    MSG msg;
        
    while (true) {
        if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
            if (msg.message == WM_QUIT) 
                break;
                
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
            
        // draw(); <- there goes your drawing
            
        SwapBuffers(dc);
    }
    /* ********** */
        
    return 0;
}

// Procedure that processes window events
LRESULT CALLBACK window_procedure(HWND window_handle, UINT message, WPARAM param_w, LPARAM param_l)
{
    /* When destroying the dummy window, WM_DESTROY message is going to be sent,
       but we don't want to quit the application then, and that is controlled by
       the quit flag. */

    switch(message) {
    case WM_DESTROY:
        if (!quit) quit = true;
        else PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(window_handle, message, param_w, param_l);
}

/* A procedure for getting OpenGL functions and OpenGL or WGL extensions.
   When looking for OpenGL 1.2 and above, or extensions, it uses wglGetProcAddress,
   otherwise it falls back to GetProcAddress. */
void* get_proc(const char *proc_name)
{
    void *proc = (void*)wglGetProcAddress(proc_name);
    if (!proc) proc = (void*)GetProcAddress(gl_module, proc_name);

    return proc;
}

使用帶有 MinGW / Cygwin 的 g++ GLExample.cpp -lopengl32 -lgdi32 或帶有 MSVC 編譯器的 cl GLExample.cpp opengl32.lib gdi32.lib user32.lib 編譯。但請確保 OpenGL 登錄檔中的標頭位於包含路徑中。如果沒有,請使用 g++的 -I 標誌或 cl 的/I,以告訴編譯器它們在哪裡。