在 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,以告诉编译器它们在哪里。