OpenGL教程之新手上路
Jeff Molofee(NeHe)的OpenGL教程
- 新手上路
译者的话:NeHe的教程一共有30多课,内容翔实,并且不断更新 。国内的网站实在应该向他们学习。令人惊讶的是,NeHe提供的例程源码几乎都有跨平台的不同编译版本,涉及从Visual C++、Borland C++、Visual Basic、MacOS X/GLUT、Linux/GLX、Code Warrior、Delphi、C++ Builder、MASM、ASM、MingW32&Allegro以及Python等等的不同平台下的多种编译器。这在国内市场上的百元大书中似乎也未曾见到。关于OpenGL,是最早由SGI开发的跨平台的工业标准的3D图形硬件的软件接口,与微软的DirectX所鼎立,不必我来多吹。
由于CKER只是业余水准,关于OpenGL的专用术语的翻译难免有错误和不妥之处,请多加指正。另外,要想流畅的运行例程,您的爱机应该够劲,内存应该够大,还要支持3D硬件加速的显卡。第一课的内容有些简单,但这是NeHe几乎所有的OpenGl例程的框架。他有太废话,但看看不会错的。
原 文:Lesson 1: Setting Up OpenGL In Windows
译 者:CKER
欢迎来到我的OpenGL教程。我是个对OpenGL充满激情的普通男孩。我第一次听说OpenGL是3Dfx发布Voodoo1卡的OpenGL硬件加速驱动的时候。我立刻意识到OpenGL是那种必须学习的东西。不幸的是当时很难从书本或网络上找到关于OpenGL的讯息。我花了N个小时来调试自己书写的代码,甚至在IRC和e-Mail上花更多的时间来恳求别人帮忙。但我发现那些懂得OpenGL高手们保留了他们的精华,对共享知识也不感兴趣。实在让人灰心。
我创建这个网站的目的是为了帮助那些对OpenGL有兴趣却又需要帮助的人。在我的每个教程中,我都会尽可能详细的来解释每一行代码的作用。我会努力让我的代码更简单(您无需学习MFC代码)。就算您是个VC、OpenGL的绝对新手也应该可以读通代码,并清楚的知道发生了什么。我的站点只是许多提供OpenGL教程的站点中的一个。如果您是OpenGL的高级程序员的话,我的站点可能太简单了,但如果您才开始的话,我想这个站点会教会您许多东西。
教程的这一节在2000年一月彻底重写了一遍。将会教您如何设置一个OpenGL窗口。它可以只是一个窗口或是全屏幕的、可以任意大小、任意色彩深度。此处的代码很稳定且很强大,您可以在您所有的OpenGL项目中使用。我所有的教程都将基于此节的代码。所有的错误都有被报告。所以应该没有内存泄漏,代码也很容易阅读和修改。感谢Fredric Echols对代码所做的修改。
现在就让我们直接从代码开始吧。第一件事是打开VC然后创建一个新工程。如果您不知道如何创建的话,您也许不该学习OpenGL,而应该先学学VC。文末可供下载的代码是VC++ 6.0的。某些版本的VC需要将bool改成BOOL,true改成TRUE,false改成FALSE,请自行修改。我用VC4和VC5编译过这些代码,没有发现问题。
在您创建一个新的Win32程序(不是console控制台程序)后,您还需要链接OpenGL库文件。在VC中操作如下:Project > Settings,然后单击LINK标签。在“Object/Library Modules”选项中的开始处(在kernel32.lib 前)增加OpenGL32.lib、GLu32.lib及GLaux.lib后单击OK按钮。现在可以开始写您的OpenGL程序了。
代码的前4行包括了我们使用的每个库文件的头文件。如下所示:
#include <windows.h> // Windows的头文件
#include <gl/gl.h> // OpenGL32库的头文件
#include <gl/glu.h> // GLu32库的头文件
#include <gl/glaux.h> // GLaux库的头文件
接下来您需要设置您计划在您的程序中使用的所有变量。本节中的例程将创建一个空的OpenGL窗口,因此我们暂时还无需设置大堆的变量。余下需要设置的变量不多,但十分重要。您将会在您以后所写的每一个OpenGL程序中用到它们。
第一行设置的变量是着色描述表(Rendering Context)。每一个OpenGL都被连接到一个着色描述表上。着色描述表将所有的OpenGL调用命令连接到设备描述表(Device Context)上。我将OpenGL的着色描述表定义为hRC。要让您的程序能够绘制窗口的话,还需要创建一个设备描述表,也就是第二行的内容。Windows的设备描述表被定义为 hDC。DC将窗口连接到图形设备接口GDI(Graphics Device Interface)。而RC将OpenGL连接到DC。第三行的变量hWnd将保存由Windows给我们的窗口指派的句柄。最后,第四行为我们的程序创建了一个Instance(实例)。
HGLRC hRC=NULL; // 永久着色描述表
HDC hDC=NULL; // 私有GDI设备描述表
HWND hWnd=NULL; // 保存我们的窗口句柄
HINSTANCE hInstance; // 保存程序的实例
下面的第一行设置一个用来监控键盘动作的数组。有许多方法可以监控键盘的动作,但这里的方法很可靠,并且可以处理多个键同时按下的情况。
active 变量用来告知程序窗口是否处于最小化的状态。如果窗口已经最小化的话,我们可以做从暂停代码执行到退出程序的任何事情。我喜欢暂停程序。这样可以使得程序不用在后台保持运行。
fullscreen 变量的作用相当明显。如果我们的程序在全屏状态下运行,fullscreen 的值为TRUE,否则为FALSE。这个全局变量的设置十分重要,它让每个过程都知道程序是否运行在全屏状态下。
bool keys[256]; // 用于键盘例程的数组
bool active=TRUE; // 窗口的活动标志,缺省为TRUE
bool fullscreen=TRUE; // 全屏标志缺省设定成全屏模式
现在我们需要先定义WndProc()。必须这么做的原因是CreateGLWindow()有对WndProc()的引用,但WndProc()在CreateGLWindow()之后才出现。在C语言中,如果我们想要访问一个当前程序段之后的过程和程序段的话,必须在程序开始处先申明所要访问的程序段。所以下面的一行代码先行定义了WndProc(),使得CreateGLWindow()能够引用WndProc()。
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // WndProc的定义
下面的代码的作用是重新设置OpenGL场景的大小,而不管窗口的大小是否已经改变(假定您没有使用全屏模式)。甚至您无法改变窗口的大小时(例如您在全屏模式下),它至少仍将运行一次 — 在程序开始时设置我们的透视图。OpenGL场景的尺寸将被设置成它显示时所在窗口的大小。
GLvoid ReSizeGLScene(GLsizei width, GLsizei height) // 重置并初始化GL窗口大小
{
if (height==0) // 防止被零除
{
height=1; // 将Height设为1
}
glViewport(0, 0, width, height); // 重置当前的视口(Viewport)
下面几行为透视图设置屏幕。意味着越远的东西看起来越小。这么做创建了一个现实外观的场景。此处透视按照基于窗口宽度和高度的45度视角来计算。
0.1f
,
100.0f
是我们在场景中所能绘制深度的起点和终点。
glMatrixMode(GL_PROJECTION)指明接下来的两行代码将影响投影矩阵(projection matrix)。投影矩阵负责为我们的场景增加透视。glLoadIdentity()近似于重置。它将所选的矩阵状态恢复成其原始状态。调用 glLoadIdentity()之后我们为场景设置透视图。glMatrixMode(GL_MODELVIEW)指明任何新的变换将会影响模型观察矩阵(modelview matrix)。模型观察矩阵中存放了我们的物体讯息。最后我们重置模型观察矩阵。如果您还不能理解这些术语的含义,请别着急。在以后的教程里,我会向大家解释。只要知道如果您想获得一个精彩的透视场景的话,必须这么做。
glMatrixMode(GL_PROJECTION); // 选择投影矩阵
glLoadIdentity(); // 重置投影矩阵
// 计算窗口的外观比例
gluPerspective(
45.0f
,(GLfloat)width/(GLfloat)height,
0.1f
,
100.0f
);
glMatrixMode(GL_MODELVIEW); // 选择模型观察矩阵
glLoadIdentity(); // 重置模型观察矩阵
}
接下的代码段中,我们将对OpenGL进行所有的设置。我们将设置清除屏幕所用的颜色,打开深度缓存,启用阴影平滑(smooth shading),等等。这个例程直到OpenGL窗口创建之后才会被调用。此过程将有返回值。但我们此处的初始化没那么复杂,现在还用不着担心这个返回值。
int InitGL(GLvoid) // 此处开始对OpenGL进行所有设置
{
下一行启用阴影平滑(smooth shading)。阴影平滑通过多边形精细的混合色彩,并对外部光进行平滑。我将在另一个教程中更详细的解释阴影平滑。
glShadeModel(GL_SMOOTH); // 启用阴影平滑
下一行设置清除屏幕时所用的颜色。如果您对色彩的工作原理不清楚的话,我快速解释一下。色彩值的范围从
0.0f
到
1.0f
。
0.0f
代表最黑的情况,
1.0f
就是最亮的情况。glClearColor后的第一个参数是红色分量(Red Intensity),第二个是绿色,第三个是蓝色。最大值也是
1.0f
,代表特定颜色分量的最亮情况。最后一个参数是Alpha值。当它用来清除屏幕的时候,我们不用关心第四个数字。现在让它为
0.0f
。我会用另一个教程来解释这个参数。
通过混合三种原色(红、绿、蓝),您可以得到不同的色彩。希望您在学校里学过这些。因此,当您使用glClearColor(
0.0f
,
0.0f
,
1.0f
,
0.0f
),您将用亮蓝色来清除屏幕。如果您用 glClearColor(
0.5f
,
0.0f
,
0.0f
,
0.0f
)的话,您将使用中红色来清除屏幕。不是最亮(
1.0f
),也不是最暗(
0.0f
)。要得到白色背景,您应该将所有的颜色设成最亮(
1.0f
)。要黑色背景的话,您该将所有的颜色设为最暗(
0.0f
)。
glClearColor(
0.0f
,
0.0f
,
0.0f
,
0.0f
); // 黑色背景
接下来的三行必须做的是关于深度缓存(depth buffer)的。将深度缓存设想为屏幕后面的层。深度缓存不断的对物体进入屏幕内部有多深进行跟踪。我们本节的程序其实没有真正使用深度缓存,但几乎所有在屏幕上显示3D场景OpenGL程序都使用深度缓存。它的排序决定那个物体先画。这样您就不会将一个圆形后面的正方形画到圆形上来。深度缓存是OpenGL十分重要的部分。
glClearDepth(
1.0f
); // 设置深度缓存
glEnable(GL_DEPTH_TEST); // 启用深度测试
glDepthFunc(GL_LEQUAL); // 所作深度测试的类型
接着告诉OpenGL我们希望进行最好的透视修正。这会十分轻微的影响性能。但使得透视图看起来好一点。
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // 真正精细的透视修正
最后,我们返回TRUE。如果我们希望检查初始化是否OK,我们可以查看返回的 TRUE或FALSE的值。如果有错误发生的话,您可以加上您自己的代码返回FALSE。目前,我们不管它。
return TRUE; // 初始化 OK
}
下一段包括了所有的绘图代码。任何您所想在屏幕上显示的东东都将在此段代码中出现。以后的每个教程中我都会在例程的此处增加新的代码。如果您对OpenGL已经有所了解的话,您可以在 glLoadIdentity()调用之后,返回TRUE值之前,试着添加一些OpenGL代码来创建基本的形。如果您是OpenGL新手,等着我的下个教程。目前我们所作的全部就是将屏幕清除成我们前面所决定的颜色,清除深度缓存并且重置场景。我们仍没有绘制任何东东。
返回TRUE值告知我们的程序没有出现问题。如果您希望程序因为某些原因而中止运行,在返回TRUE值之前增加返回FALSE的代码告知我们的程序绘图代码出错。程序即将退出。
int DrawGLScene(GLvoid) // 从这里开始进行所有的绘制
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清除屏幕和深度缓存
glLoadIdentity(); // 重置当前的模型观察矩阵
return TRUE; // 一切 OK
}
下一段代码只在程序退出之前调用。KillGLWindow()的作用是依次释放着色描述表,设备描述表和窗口句柄。我已经加入了许多错误检查。如果程序无法销毁窗口的任意部分,都会弹出带相应错误消息的讯息窗口,告诉您什么出错了。使您在您的代码中查错变得更容易些。
GLvoid KillGLWindow(GLvoid) // 正常销毁窗口
{
我们在KillGLWindow()中所作的第一件事是检查我们是否处于全屏模式。如果是,我们要切换回桌面。我们本应在禁用全屏模式前先销毁窗口,但在某些显卡上这么做可能会使得桌面崩溃。所以我们还是先禁用全屏模式。这将防止桌面出现崩溃,并在Nvidia和3DFX显卡上都工作的很好。
if (fullscreen) // 我们处于全屏模式吗?
{
我们使用ChangeDisplaySettings(NULL,0)回到原始桌面。将NULL作为第一个参数,0作为第二个参数传递强制Windows使用当前存放在注册表中的值(缺省的分辨率、色彩深度、刷新频率,等等)来有效的恢复我们的原始桌面。切换回桌面后,我们还要使得鼠标指针重新可见。
ChangeDisplaySettings(NULL,0); // 是的话,切换回桌面
ShowCursor(TRUE); // 显示鼠标指针
}
接下来的代码查看我们是否拥有着色描述表(hRC)。如果没有,程序将跳转至后面的代码查看是否拥有设备描述表。
if (hRC) // 我们拥有着色描述表吗?
{
如果存在着色描述表的话,下面的代码将查看我们能否释放它(将hRC从hDC分开)。这里请注意我使用的查错方法。基本上我只是让程序尝试释放着色描述表(通过调用:wglMakeCurrent(NULL,NULL)),然后我再查看释放是否成功。巧妙的将数行代码结合到了一行。
if (!wglMakeCurrent(NULL,NULL)) // 我们能否释放DC和RC描述表?
{
如果不能释放DC和RC描述表的话,MessageBox()将弹出错误消息,告知我们DC和RC无法被释放。NULL意味着消息窗口没有父窗口。其右的文字将在消息窗口上出现。“SHUTDOWN ERROR”出现在窗口的标题栏上。MB_OK的意思消息窗口上带有一个写着OK字样的按钮。
MB_ICONINFORMATION将在消息窗口中显示一个带圈的小写的i(看上去更正式一些)。
MessageBox(NULL,"Release Of DC And RC Failed.",
"SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
}
下一步我们试着删除着色描述表。如果不成功的话弹出错误消息。
if (!wglDeleteContext(hRC)) // 我们能否删除RC?
{
如果无法删除着色描述表的话,将弹出错误消息告知我们RC未能成功删除。然后hRC被设为NULL。
MessageBox(NULL,"Release Rendering Context Failed.",
"SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
}
hRC=NULL; // 将RC设为 NULL
}
现在我们查看是否存在设备描述表,如果有尝试释放它。如果不能释放设备描述表将弹出错误消息,然后hDC设为NULL。
if (hDC && !ReleaseDC(hWnd,hDC)) // 我们能否释放 DC?
{
MessageBox(NULL,"Release Device Context Failed.",
"SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
hDC=NULL; // 将 DC 设为 NULL
}
现在我们来查看是否存在窗口句柄,我们调用DestroyWindow( hWnd )来尝试销毁窗口。如果不能的话弹出错误窗口,然后hWnd被设为NULL。
if (hWnd && !DestroyWindow(hWnd)) // 能否销毁窗口?
{
MessageBox(NULL,"Could Not Release hWnd.",
"SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
hWnd=NULL; // 将 hWnd 设为 NULL
}
最后要做的事是注销我们的窗口类。这允许我们正常销毁窗口,接着在打开其他窗口时,不会收到诸如“Windows Class already registered”(窗口类已注册)的错误消息。
if (!UnregisterClass("OpenGL",hInstance)) // 能否注销类?
{
MessageBox(NULL,"Could Not Unregister Class.",
"SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
hInstance=NULL; // 将 hInstance 设为 NULL
}
}
接下来的代码段创建我们的OpenGL窗口。我花了很多时间来做决定是否创建固定的全屏模式这样不需要许多额外的代码,还是创建一个容易定制的友好的窗口但需要更多的代码。当然最后我选择了后者。我经常在EMail中收到诸如此类的问题:怎样创建窗口而不使用全屏幕?怎样改变窗口的标题栏?怎样改变窗口的分辨率或象素格式(pixel format)?以下的代码完成了所有这一切。尽管最好要学学材质,这会让您写自己的OpenGL程序变得容易的多。
正如您所见,此过程返回布尔变量(TRUE或FALSE)。他还带有5个参数:窗口的标题栏,窗口的宽度,窗口的高度,色彩位数(16/24/32),和全屏标志(TRUE — 全屏模式,FALSE — 窗口模式)。返回的布尔值告诉我们窗口是否成功创建。
BOOL CreateGLWindow(char* title, int width, int height, int bits, bool fullscreenflag)
{
当我们要求Windows为我们寻找相匹配的象素格式时,Windows寻找结束后将模式值保存在变量PixelFormat中。
GLuint PixelFormat; // 保存查找匹配的结果
wc用来保存我们的窗口类的结构。窗口类结构中保存着我们的窗口信息。通过改变类的不同字段我们可以改变窗口的外观和行为。每个窗口都属于一个窗口类。当您创建窗口时,您必须为窗口注册类。
WNDCLASS wc; // 窗口类结构
dwExStyle和dwStyle存放扩展和通常的窗口风格信息。我使用变量来存放风格的目的是为了能够根据我需要创建的窗口类型(是全屏幕下的弹出窗口还是窗口模式下的带边框的普通窗口);来改变窗口的风格。
DWORD dwExStyle; // 扩展窗口风格
DWORD dwStyle; // 窗口风格
下面的5行代码取得矩形的左上角和右下角的坐标值。我们将使用这些值来调整我们的窗口使得其上的绘图区的大小恰好是我们所需的分辨率的值。通常如果我们创建一个640x480的窗口,窗口的边框会占掉一些分辨率的值。
RECT WindowRect; // 取得矩形的左上角和右下角的坐标值
WindowRect.left=(long)0; // 将Left 设为 0
WindowRect.right=(long)width; // 将Right 设为要求的宽度
WindowRect.top=(long)0; // 将Top 设为 0
WindowRect.bottom=(long)height; // 将Bottom 设为要求的高度
下一行代码我们让全局变量fullscreen等于fullscreenflag。如果我们希望在全屏幕下运行而将fullscreenflag设为TRUE,但没有让变量fullscreen等于fullscreenflag的话,fullscreen变量将保持为FALSE。当我们在全屏幕模式下销毁窗口的时候,变量fullscreen的值却不是正确的TRUE值,计算机将误以为已经处于桌面模式而无法切换回桌面。上帝啊,但愿这一切都有意义。就是一句话,fullscreen的值必须永远fullscreenflag的值,否则就会有问题。(CKER也觉得此处太废话,懂的人都要不懂啦.....:( )
fullscreen=fullscreenflag; // 设置全局全屏标志
下一部分的代码中,我们取得窗口的实例,然后定义窗口类。
CS_HREDRAW和CS_VREDRAW 的意思是无论何时,只要窗口发生变化时就强制重画。CS_OWNDC为窗口创建一个私有的DC。这意味着DC不能在程序间共享。WndProc是我们程序的消息处理过程。由于没有使用额外的窗口数据,后两个字段设为零。然后设置实例。接着我们将hIcon设为NULL,因为我们不想给窗口来个图标。鼠标指针设为标准的箭头。背景色无所谓(我们在GL中设置)。我们也不想要窗口菜单,所以将其设为NULL。类的名字可以您想要的任何名字。出于简单,我将使用“OpenGL”。
hInstance = GetModuleHandle(NULL); // 取得我们窗口的实例
wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; // 移动时重画,并为窗口取得DC
wc.lpfnWndProc = (WNDPROC) WndProc; // WndProc处理消息
wc.cbClsExtra = 0; // 无额外窗口数据
wc.cbWndExtra = 0; // 无额外窗口数据
wc.hInstance = hInstance; // 设置实例
wc.hIcon = LoadIcon(NULL, IDI_WINLOGO); // 装入缺省图标
wc.hCursor = LoadCursor(NULL, IDC_ARROW); // 装入鼠标指针
wc.hbrBackground = NULL; // GL不需要背景
wc.lpszMenuName = NULL; // 不需要菜单
wc.lpszClassName = "OpenGL"; // 设定类名字
现在注册类名字。如果有错误发生,弹出错误消息窗口。按下上面的OK按钮后,程序退出。
if (!RegisterClass(&wc)) // 尝试注册窗口类
{
MessageBox(NULL,"Failed To Register The Window Class.","ERROR",
MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 退出并返回FALSE
}
查看程序应该在全屏模式还是窗口模式下运行。如果应该是全屏模式的话,我们将尝试设置全屏模式。
if (fullscreen) // 要尝试全屏模式吗?
{
下一部分的代码看来很多人都会有问题要问关于......切换到全屏模式。在切换到全屏模式时,有几件十分重要的事您必须牢记。必须确保您在全屏模式下所用的宽度和高度等同于窗口模式下的宽度和高度。最最重要的是要在创建窗口之前设置全屏模式。这里的代码中,您无需再担心宽度和高度,它们已被设置成与显示模式所对应的大小。
DEVMODE dmScreenSettings; // 设备模式
memset(&dmScreenSettings,0,sizeof(dmScreenSettings)); // 确保内存分配
dmScreenSettings.dmSize=sizeof(dmScreenSettings); // Devmode 结构的大小
dmScreenSettings.dmPelsWidth = width; // 所选屏幕宽度
dmScreenSettings.dmPelsHeight = height; // 所选屏幕高度
dmScreenSettings.dmBitsPerPel = bits; // 每象素所选的色彩深度
dmScreenSettings.dmFields = DM_BITSPERPEL|DM_PELSWIDTH|DM_PELSHEIGHT;
上面的代码中,我们分配了用于存储视频设置的空间。设定了屏幕的宽,高,色彩深度。下面的代码我们尝试设置全屏模式。我们在dmScreenSettings中保存了所有的宽,高,色彩深度讯息。下一行使用ChangeDisplaySettings来尝试切换成与dmScreenSettings所匹配模式。我使用参数CDS_FULLSCREEN来切换显示模式,因为这样做不仅移去了屏幕底部的状态条,而且它在来回切换时,没有移动或改变您在桌面上的窗口。
// 尝试设置显示模式并返回结果。注: CDS_FULLSCREEN 移去了状态条。
if (ChangeDisplaySettings(&dmScreenSettings,
CDS_FULLSCREEN)!=DISP_CHANGE_SUCCESSFUL)
{
如果模式未能设置成功,我们将进入以下的代码。如果不能匹配全屏模式,弹出消息窗口,提供两个选项:在窗口模式下运行或退出。
// 若模式失败,提供两个选项:退出或在窗口内运行。
if (MessageBox(NULL,
"The Requested Fullscreen Mode Is Not Supported By/n
Your Video Card. Use Windowed Mode Instead?","NeHe GL",
MB_YESNO|MB_ICONEXCLAMATION)==IDYES)
{
如果用户选择窗口模式,变量fullscreen 的值变为FALSE,程序继续运行。
fullscreen=FALSE; // 选择窗口模式(Fullscreen=FALSE)
}
else
{
如果用户选择退出,弹出消息窗口告知用户程序将结束。并返回FALSE告诉程序窗口未能成功创建。程序退出。
// Pop Up A Message Box Letting User Know The Program Is Closing.
MessageBox(NULL,
"Program Will Now Close.","ERROR",MB_OK|MB_ICONSTOP);
return FALSE; //退出并返回 FALSE
}
}
}
由于全屏模式可能失败,用户可能决定在窗口下运行,我们需要在设置屏幕/窗口之前,再次检查fullscreen的值是TRUE或FALSE。
if (fullscreen) // 仍处于全屏模式吗?
{
如果我们仍处于全屏模式,设置扩展窗体风格为WS_EX_APPWINDOW,这将强制我们的窗体可见时处于最前面。再将窗体的风格设为WS_POPUP。这个类型的窗体没有边框,使我们的全屏模式得以完美显示。
最后我们禁用鼠标指针。当您的程序不是交互式的时候,在全屏模式下禁用鼠标指针通常是个好主意。
dwExStyle=WS_EX_APPWINDOW; // 扩展窗体风格
dwStyle=WS_POPUP; // 窗体风格
ShowCursor(FALSE); // 隐藏鼠标指针
}
else
{
如果我们使用窗口而不是全屏模式,我们在扩展窗体风格中增加了 WS_EX_WINDOWEDGE,增强窗体的3D感观。窗体风格改用 WS_OVERLAPPEDWINDOW,创建一个带标题栏、可变大小的边框、菜单和最大化/最小化按钮的窗体。
dwExStyle=WS_EX_APPWINDOW | WS_EX_WINDOWEDGE; // 扩展窗体风格
dwStyle=WS_OVERLAPPEDWINDOW; // 窗体风格
}
下一行代码根据创建的窗体类型调整窗口。调整的目的是使得窗口大小正好等于我们要求的分辨率。通常边框会占用窗口的一部分。使用AdjustWindowRectEx 后,我们的OpenGL场景就不会被边框盖住。实际上窗口变得更大以便绘制边框。全屏模式下,此命令无效。
AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle);// 调整窗口达到真正要求的大小
下一段代码开始创建窗口并检查窗口是否成功创建。我们将传递CreateWindowEx()所需的所有参数。如扩展风格、类名字(与您在注册窗口类时所用的名字相同)、窗口标题、窗体风格、窗体的左上角坐标(0,0是个安全的选择)、窗体的宽和高。我们没有父窗口,也不想要菜单,这些参数被设为NULL。还传递了窗口的实例,最后一个参数被设为NULL。
注意我们在窗体风格中包括了WS_CLIPSIBLINGS和WS_CLIPCHILDREN。要让OpenGL正常运行,这两个属性是必须的。他们阻止别的窗体在我们的窗体内/上绘图。
if (!(hWnd=CreateWindowEx( dwExStyle, // 扩展窗体风格
"OpenGL", // 类名字
title, // 窗口标题
WS_CLIPSIBLINGS | // 必须的窗体风格属性
WS_CLIPCHILDREN | // 必须的窗体风格属性
dwStyle, // 选择的窗体属性
0,0, // 窗口位置
WindowRect.right-WindowRect.left, // 计算调整好的窗口宽度
WindowRect.bottom-WindowRect.top, // 计算调整好的窗口高度
NULL, // 无父窗口
NULL, // 无菜单
hInstance, // 实例
NULL))) // 不向WM_CREATE传递任何东东
下来我们检查看窗口是否正常创建。如果成功,hWnd保存窗口的句柄。如果失败,弹出消息窗口,并退出程序。
{
KillGLWindow(); // 重置显示区
MessageBox(NULL,"Window Creation Error.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回 FALSE
}
下面的代码描述象素格式。我们选择了通过RGBA(红、绿、蓝、alpha通道)支持OpenGL和双缓存的格式。我们试图找到匹配我们选定的色彩深度(16位、24位、32位)的象素格式。最后设置16位Z-缓存。其余的参数要么未使用要么不重要(stencil buffer:模板缓存与accumulation buffer:聚集缓存除外)。
static PIXELFORMATDESCRIPTOR pfd= //pfd 告诉窗口我们所希望的东东
{
sizeof(PIXELFORMATDESCRIPTOR), //上诉格式描述符的大小
1, // 版本号
PFD_DRAW_TO_WINDOW | // 格式必须支持窗口
PFD_SUPPORT_OPENGL | // 格式必须支持OpenGL
PFD_DOUBLEBUFFER, // 必须支持双缓冲
PFD_TYPE_RGBA, // 申请 RGBA 格式
bits, // 选定色彩深度
0, 0, 0, 0, 0, 0, // 忽略的色彩位
0, // 无Alpha缓存
0, // 忽略Shift Bit
0, // 无聚集缓存
0, 0, 0, 0, // 忽略聚集位
16, // 16位 Z-缓存 (深度缓存)
0, // 无模板缓存
0, // 无辅助缓存
PFD_MAIN_PLANE, // 主绘图层
0, // 保留
0, 0, 0 // 忽略层遮罩
};
如果前面创建窗口时没有错误发生,我们接着尝试取得OpenGL设备描述表。若无法取得DC,弹出错误消息程序退出(返回FALSE)。
if (!(hDC=GetDC(hWnd))) // 取得设备描述表了么?
{
KillGLWindow(); // 重置显示区
MessageBox(NULL,"Can't Create A GL Device Context.",
"ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回 FALSE
}
设法为OpenGL窗口取得设备描述表后,我们尝试找到对应与此前我们选定的象素格式的象素格式。如果Windows不能找到的话,弹出错误消息,并退出程序(返回FALSE)。
if (!(PixelFormat=ChoosePixelFormat(hDC,&pfd))) // Windows 找到相应的象素格式了吗?
{
KillGLWindow(); // 重置显示区
MessageBox(NULL,"Can't Find A Suitable PixelFormat.",
"ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回 FALSE
}
Windows找到相应的象素格式后,尝试设置象素格式。如果无法设置,弹出错误消息,并退出程序(返回FALSE)。
if(!SetPixelFormat(hDC,PixelFormat,&pfd)) // 能够设置象素格式么?
{
KillGLWindow(); // 重置显示区
MessageBox(NULL,"Can't Set The PixelFormat.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回 FALSE
}
正常设置象素格式后,尝试取得着色描述表。如果不能取得着色描述表的话,弹出错误消息,并退出程序(返回FALSE)。
if (!(hRC=wglCreateContext(hDC))) // 能否取得着色描述表?
{
KillGLWindow(); // 重置显示区
MessageBox(NULL,"Can't Create A GL Rendering Context.",
"ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回 FALSE
}
如果到现在仍未出现错误的话,我们已经设法取得了设备描述表和着色描述表。接着要做的是激活着色描述表。如果无法激活,弹出错误消息,并退出程序(返回FALSE)。
if(!wglMakeCurrent(hDC,hRC)) // 尝试激活着色描述表
{
KillGLWindow(); // 重置显示区
MessageBox(NULL,"Can't Activate The GL Rendering Context.",
"ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回 FALSE
}
一切顺利的话,OpenGL窗口已经创建完成,接着可以显示它啦。将它设为前端窗口(给它更高的优先级),并将焦点移至此窗口。然后调用ReSizeGLScene 将屏幕的宽度和高度设置给透视OpenGL屏幕。
ShowWindow(hWnd,SW_SHOW); // 显示窗口
SetForegroundWindow(hWnd); // 略略提高优先级
SetFocus(hWnd); // 设置键盘的焦点至此窗口
ReSizeGLScene(width, height); // 设置透视 GL 屏幕
跳转至InitGL(),这里可以设置光照、纹理、等等任何需要设置的东东。您可以在InitGL()内部自行定义错误检查,并返回TRUE(一切正常)或FALSE(有什么不对)。例如,如果您在InitGL()内装载纹理并出现错误,您可能希望程序停止。如果您返回FALSE的话,下面的代码会弹出错误消息,并退出程序。
if (!InitGL()) // 初始化新建的GL窗口
{
KillGLWindow(); // 重置显示区
MessageBox(NULL,"Initialization Failed.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回 FALSE
}
到这里可以安全的推定创建窗口已经成功了。我们向WinMain()返回TRUE,告知WinMain()没有错误,以防止程序退出。
return TRUE; // 成功
}
下面的代码处理所有的窗口消息。当我们注册好窗口类之后,程序跳转到这部分代码处理窗口消息。
LRESULT CALLBACK WndProc( HWND hWnd, // 窗口的句柄
UINT uMsg, // 窗口的消息
WPARAM wParam, // 附加的消息内容
LPARAM lParam) // 附加的消息内容
{
下来的代码比对uMsg的值,然后转入case处理,uMsg 中保存了我们要处理的消息名字。
switch (uMsg) // 检查Windows消息
{
如果uMsg等于WM_ACTIVE,查看窗口是否仍然处于激活状态。如果窗口已被最小化,将变量active设为FALSE。如果窗口已被激活,变量active的值为TRUE。
case WM_ACTIVATE: // 监视窗口激活消息
{
if (!HIWORD(wParam)) // 检查最小化状态
{
active=TRUE; // 程序处于激活状态
}
else
{
active=FALSE; // 程序不再激活
}
return 0; // 返回消息循环
}
如果消息是WM_SYSCOMMAND(系统命令),再次比对wParam。如果wParam是SC_SCREENSAVE或SC_MONITORPOWER的话,不是有屏幕保护要运行,就是显示器想进入节电模式。返回0可以阻止这两件事发生。
case WM_SYSCOMMAND: // 中断系统命令Intercept System Commands
{
switch (wParam) // 检查系统调用Check System Calls
{
case SC_SCREENSAVE: // 屏保要运行?
case SC_MONITORPOWER: // 显示器要进入节电模式?
return 0; // 阻止发生
}
break; // 退出
}
如果uMsg是WM_CLOSE,窗口将被关闭。我们发出退出消息,主循环将被中断。变量done被设为TRUE,WinMain()的主循环中止,程序关闭。
case WM_CLOSE: // 收到Close消息?
{
PostQuitMessage(0); // 发出退出消息
return 0;
}
如果键盘有键按下,通过读取wParam的信息可以找出键值。我将键盘数组keys[ ]相应的数组组成员的值设为TRUE。这样以后就可以查找key[ ]来得知什么键被按下。允许同时按下多个键。
case WM_KEYDOWN: // 有键按下么?
{
keys[wParam] = TRUE; // 如果是,设为TRUE
return 0; // 返回
}
同样,如果键盘有键释放,通过读取wParam的信息可以找出键值。然后将键盘数组keys[ ]相应的数组组成员的值设为FALSE。这样查找key[ ]来得知什么键被按下,什么键被释放了。键盘上的每个键都可以用0~255之间的一个数来代表。举例来说,当我们按下40所代表的键时,keys[40]的值将被设为TRUE。放开的话,它就被设为FALSE。这也是key数组的原理。
case WM_KEYUP: // 有键放开么?
{
keys[wParam] = FALSE; // 如果是,设为FALSE
return 0; // 返回
}
当调整窗口时,uMsg最后等于消息WM_SIZE。读取lParam的LOWORD和HIWORD可以得到窗口新的宽度和高度。将他们传递给ReSizeGLScene(),OpenGL场景将调整为新的宽度和高度。
case WM_SIZE:
{
ReSizeGLScene(LOWORD(lParam),HIWORD(lParam));
return 0;
}
}
其余无关的消息被传递给DefWindowProc,让Windows自行处理。
// 向 DefWindowProc传递所有未处理的消息。
return DefWindowProc(hWnd,uMsg,wParam,lParam);
}
下面是我们的Windows程序的入口。将会调用窗口创建例程,处理窗口消息,并监视人机交互。
int WINAPI WinMain( HINSTANCE hInstance, // 实例
HINSTANCE hPrevInstance, // 前一个实例
LPSTR lpCmdLine, // 命令行参数
int nCmdShow) // 窗口显示状态
{
我们设置两个变量。msg用来检查是否有消息等待处理。done的初始值设为FALSE。这意味着我们的程序仍未完成运行。只要程序done保持FALSE,程序继续运行。一旦done的值改变为TRUE,程序退出。
MSG msg; // Windowsx消息结构
BOOL done=FALSE; // 用来退出循环的Bool 变量
这段代码完全可选。程序弹出一个消息窗口,询问用户是否希望在全屏模式下运行。如果用户单击NO按钮,fullscreen变量从缺省的TRUE改变为FALSE,程序也改在窗口模式下运行。
// 提示用户选择运行模式
if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?",
"Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)
{
fullscreen=FALSE; // 窗口模式
}
接着创建OpenGL窗口。CreateGLWindow函数的参数依次为标题、宽度、高度、色彩深度,以及全屏标志。就这么简单。我很欣赏这段代码的简洁。如果未能创建成功,函数返回FALSE。程序立即退出。
// 创建OpenGL窗口
if (!CreateGLWindow("NeHe's OpenGL Framework",640,480,16,fullscreen))
{
return 0; // 失败退出
}
下面是循环的开始。只要done保持FALSE,循环一直进行。
while(!done) // 保持循环直到 done=TRUE
{
我们要做的第一件事是检查是否有消息在等待。使用PeekMessage()可以在不锁住我们的程序的前提下对消息进行检查。许多程序使用GetMessage(),也可以很好的工作。但使用GetMessage(),程序在收到paint消息或其他别的什么窗口消息之前不会做任何事。
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) //有消息在等待吗?
{
下面的代码查看是否出现退出消息。如果当前的消息是由PostQuitMessage(0)引起的WM_QUIT,done变量被设为TRUE,程序将退出。
if (msg.message==WM_QUIT) // 收到退出消息?
{
done=TRUE; // 是,则done=TRUE
}
else // 不是,处理窗口消息
{
如果不是退出消息,我们翻译消息,然后发送消息,使得WndProc()或Windows能够处理他们。
TranslateMessage(&msg); // 翻译消息
DispatchMessage(&msg); // 发送消息
}
}
else // 如果没有消息
{
如果没有消息,绘制我们的OpenGL场景。代码的第一行查看窗口是否激活。如果按下ESC键,done变量被设为TRUE,程序将会退出。
// 绘制场景。监视ESC键和来自DrawGLScene()的退出消息
if (active) // 程序激活的么?
{
if (keys[VK_ESCAPE]) // ESC 按下了么?
{
done=TRUE; // ESC 发出退出信号
}
else // 不是退出的时候,刷新屏幕
{
如果程序是激活的且ESC没有按下,我们绘制场景并交换缓存(使用双缓存可以实现无闪烁的动画)。我们实际上在另一个看不见的“屏幕”上绘图。当我们交换缓存后,我们当前的屏幕被隐藏,现在看到的是刚才看不到的屏幕。这也是我们看不到场景绘制过程的原因。场景只是即时显示。
DrawGLScene(); // 绘制场景
SwapBuffers(hDC); // 交换缓存 (双缓存)
}
}
下面的一点代码是最近新加的(
05-01-2000
)。允许用户按下F1键在全屏模式和窗口模式间切换。
if (keys[VK_F1]) // F1键按下了么?
{
keys[VK_F1]=FALSE; // 若是,使对应的Key数组中的值为 FALSE
KillGLWindow(); // 销毁当前的窗口
fullscreen=!fullscreen; // 切换 全屏 / 窗口 模式
// 重建 OpenGL 窗口
if (!CreateGLWindow("NeHe's OpenGL Framework",
640,480,16,fullscreen))
{
return 0; // 如果窗口未能创建,程序退出
}
}
}
}
如果done变量不再是FALSE,程序退出。正常销毁OpenGL窗口,将所有的内存释放,退出程序。
// 关闭程序
KillGLWindow(); // 销毁窗口
return (msg.wParam); // 退出程序
}
在这一课中,我已试着尽量详细解释一切。每一步都与设置有关,并创建了一个全屏OpenGL程序。当您按下ESC键程序就会退出,并监视窗口是否激活。我花了整整2周时间来写代码,一周时间来改正BUG并讨论编程指南,2天(整整22小时来写HTML文件)。如果您有什么意见或建议请给我电子邮件。如果您认为有什么不对或可以改进,请告诉我。我想做最好的OpenGL教程并对您的反馈感兴趣。
译者:我也花了三天的业余时间来翻译、打字。NeHe的文档似乎很简单,似乎很罗嗦。但仔细想来这样的高手您又见过几个?还是那句话,我不是高手,希望您是,真诚的。
更多推荐
所有评论(0)