在 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.dllgdi32.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.hGL/glcorearb.hGL/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");

我們實際上可以構建一個使用 wglGetProcAddressGetProcAddress 的包裝器 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.hGL/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,以告訴編譯器它們在哪裡。