跳至内容

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。编译并运行程序,以获得以下输出。

华夏公益教科书