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项目中使用。我全部的教程都将基于此节的代码。全部的错误都有被报告。所以应该没有内存泄漏,代码也非常easy阅读和改动。感谢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后单击OKbutton。如今能够開始写您的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度视角来计算。
glMatrixMode(GL_PROJECTION)指明接下来的两行代码将影响投影矩阵(projection matrix)。投影矩阵负责为我们的场景添加透视。glLoadIdentity()近似于重置。它将所选的矩阵状态恢复成其原始状态。调用 glLoadIdentity()之后我们为场景设置透视图。glMatrixMode(GL_MODELVIEW)指明不论什么新的变换将会影响模型观察矩阵(modelview matrix)。模型观察矩阵中存放了我们的物体讯息。最后我们重置模型观察矩阵。假设您还不能理解这些术语的含义,请别着急。在以后的教程里,我会向大家解释。仅仅要知道假设您想获得一个精彩的透视场景的话,必须这么做。
glMatrixMode(GL_PROJECTION); // 选择投影矩阵
glLoadIdentity(); // 重置投影矩阵
// 计算窗体的外观比例
gluPerspective(
glMatrixMode(GL_MODELVIEW); // 选择模型观察矩阵
glLoadIdentity(); // 重置模型观察矩阵
}
接下的代码段中,我们将对OpenGL进行全部的设置。我们将设置清除屏幕所用的颜色,打开深度缓存,启用阴影平滑(smooth shading),等等。这个例程直到OpenGL窗体创建之后才会被调用。此过程将有返回值。但我们此处的初始化没那么复杂,如今还用不着操心这个返回值。
int InitGL(GLvoid) // 此处開始对OpenGL进行全部设置
{
下一行启用阴影平滑(smooth shading)。阴影平滑通过多边形精细的混合色彩,并对外部光进行平滑。我将在还有一个教程中更具体的解释阴影平滑。
glShadeModel(GL_SMOOTH); // 启用阴影平滑
下一行设置清除屏幕时所用的颜色。假设您对色彩的工作原理不清楚的话,我高速解释一下。色彩值的范围从
通过混合三种原色(红、绿、蓝),您能够得到不同的色彩。希望您在学校里学过这些。因此,当您使用glClearColor(
glClearColor(
接下来的三行必须做的是关于深度缓存(depth buffer)的。将深度缓存设想为屏幕后面的层。深度缓存不断的对物体进入屏幕内部有多深进行跟踪。我们本节的程序事实上没有真正使用深度缓存,但差点儿全部在屏幕上显示3D场景OpenGL程序都使用深度缓存。它的排序决定那个物体先画。这样您就不会将一个圆形后面的正方形画到圆形上来。深度缓存是OpenGL十分重要的部分。
glClearDepth(
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()的作用是依次释放着色描写叙述表,设备描写叙述表和窗体句柄。我已经加入了很多错误检查。假设程序无法销毁窗体的随意部分,都会弹出带对应错误消息的讯息窗体,告诉您什么出错了。使您在您的代码中查错变得更easy些。
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字样的button。
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窗体。我花了非常多时间来做决定是否创建固定的全屏模式这样不须要很多额外的代码,还是创建一个easy定制的友好的窗体但须要很多其它的代码。当然最后我选择了后者。我常常在EMail中收到诸如此类的问题:如何创建窗体而不使用全屏幕?如何改变窗体的标题栏?如何改变窗体的分辨率或象素格式(pixel format)?以下的代码完毕了全部这一切。虽然最好要学学材质,这会让您写自己的OpenGL程序变得easy的多。
正如您所见,此过程返回布尔变量(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"; // 设定类名字
如今注冊类名字。假设有发生错误,弹出错误消息窗体。按下上面的OKbutton后,程序退出。
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,创建一个带标题栏、可变大小的边框、菜单和最大化/最小化button的窗体。
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 变量
这段代码全然可选。程序弹出一个消息窗体,询问用户是否希望在全屏模式下执行。假设用户单击NObutton,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); // 交换缓存 (双缓存)
}
}
以下的一点代码是近期新加的(
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的文档似乎非常简单,似乎非常罗嗦。但细致想来这种高手您又见过几个?还是那句话,我不是高手,希望您是,真诚的。