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