OpenGL 编程/GLStart/Tut2
使用第一课中的代码(Win32 入门),我们将设置 Windows 程序以使用 OpenGL。首先,启动我们在第一课中使用的 Dev - C++ 项目。
打开 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。编译并运行程序,以获得以下输出。