原文: Setting Up An OpenGL Window
欢迎阅读我的OpenGL教程.我是一个热爱OpenGL的普通码农!我第一次听到OpenGL是在3Dfx刚发布他们给Voodoo I显卡的OpenGL硬件加速驱动的时候.我马上意识到我必须学习OpenGL.不幸的是,当时在网上很难找到关于OpenGL的书和资料.我花了数小时来编写可运行的代码,并且花了更多时间去发邮件和在IRC上求教别人.但是我发现懂OpenGL的人会当自己是神,并且完全没兴趣分享他们的技术.他们真的很烦!
我创建此站是为了给有兴趣学OpenGL的人提供帮助.每个章节我都会尽我所能的去解释尽量多的细节,例如每行代码都写有注释.我尽量保持代码简明(不涉及到MFC)!就算是VC++和OpenGL的新手,也可以通俗理解示例代码.本站只是众多OpenGL教程站中的一个,如果你是骨灰级OpenGL程序员,本站对你来说太过简单,但如果你是初学者,我觉得本站对你很有帮助.
本教程在2000的1月的时候重写了一次.本教程会教你如何创建一个OpenGL窗体.该窗体可以是带边框的窗体或者全屏,或者任何你想要的大小,分辨率和色深.代码的可扩展性很高,也可以用在你自己的OpenGL项目中.整个教程都会基于这一节的代码!所以我把它写成可扩展和实用性强的.所有错误都会被报告.代码应该是没内存泄漏,也比较容易读懂和修改.感谢Fredic Echols提交的修改代码!
我会从代码开始讲解.你要做的第一件事是在VC++下创建项目.如果你不懂怎么创建,你应该先学习VC++.提供下载的代码是VC++ 6.0代码.而有些VC++版本会需要把bool转换为大写,true和false也转换为大写.为了解决上述更改,我已经把代码修改成可以在VC++ 4.0和5.0下编译.
等你在VC++创建一个新的Win32应用(非控制台应用)之后,你会要链接到OpenGL库.在VC++中是到项目->设置,然后右键点击LINK 选项卡.在"Object/Library Modules"的第一行(在kernel32.lib前面)添加OpenGL32.lib GLu32.lib和GLaux.lib.然后按确定,然后你就能开始写OpenGL窗体程序了.
注意1: 很多编译器没有定义CDS_FULLSCREEN. 如果你收到一条错误提示是关于CDS_FULLSCREEN的话,你就要添加以下代码到你程序的头部: #define CDS_FULLSCREEN 4.
注意2: 写本教程的第一版时,GLAUX是可行的.之后GLAUX就停止更新了.本站的很多教程仍然使用旧的GLAUX代码.如果你的编译器不支持GLAUX,你不能用的话,可以用主页(左边菜单)提供的GLAUX替换代码.
头4行代码包含了我们用到的各个库的头文件.
#include <windows.h> // Header File For Windows #include <glgl.h> // Header File For The OpenGL32 Library #include <glglu.h> // Header File For The GLu32 Library #include <glglaux.h> // Header File For The GLaux Library
接下来你要设置在程序中用到的所有变量.该程序会创建空OpenGL窗体,所以我们暂时不需要定义太多变量.我们定义尽量少的变量是非常重要的,因为往后的示例都以本节的代码为基础扩展.
第一行定义了一个渲染上下文.所有的OpenGL程序都被链接到渲染上下文.渲染上下文的作用是把OpenGL调用链接到设备上下文.这里的OpenGL渲染上下文定义名叫hRC.要把程序绘制到窗体的话就需要设备上下文,第二行代码就是干这事.该Windows设备上下文命名为hDC.DC把窗体连接到GDI(图形设备接口).而RC连接OpenGL到DC.
在第三行,变量hWnd会保存Windows分配给我们窗体的句柄,最后,第四行代码为我们的程序创建一个实例(表现).
HGLRC hRC=NULL; // Permanent Rendering Context HDC hDC=NULL; // Private GDI Device Context HWND hWnd=NULL; // Holds Our Window Handle HINSTANCE hInstance; // Holds The Instance Of The Application
下面第一行代码是创建一个用于监控按下的键的数组.有很多途径可以观察按键事件,但是下面这种是我惯用的.这种方法比较可靠,而且可以同时控制多个键按下的事件.
active变量是用来储存窗体是否最小化到任务栏的状态.如果窗体被最小化的话我们可以暂停退出程序来做任何事.我喜欢暂停程序,这样的话最小化时后台不会持续运作.
fullscreen变量非常明显了.如果我们程序运行在全屏模式下,fullscreen的值会是TRUE,如果运行在窗体模式下,fullscreen的值是FALSE.要注意的是,该变量要定义为全局,这样的话所有函数都知道程序是否运行在全屏模式下.
bool keys[256]; // Array Used For The Keyboard Routine bool active=TRUE; // Window Active Flag Set To TRUE By Default bool fullscreen=TRUE; // Fullscreen Flag Set To Fullscreen Mode By Default
现在我们要定义WndProc函数.原因是CreateGLWindow函数会调用WndProc函数但是WndProc函数的实现在CreateGLWindow函数后面.在C语言中,如果要在一个函数里面调用一个实现代码在其后面的函数的话,必须在该函数之前先声明要调用函数的原型.所以这里先声明WndProc函数,这样CreateGLWindow函数就能调用它了.
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Declaration For WndProc
下面函数的代码片段是用来在窗体大小变更的时候更改OpenGL场景大小的(假定你是在窗体模式下).即使你不能变更窗体大小(例如在全屏模式下),该程序也至少会在程序初次运行时被调用一次,用于创建我们的视图.OpenGL场景大小的变更是基于当前显示窗体的宽高.
GLvoid ReSizeGLScene(GLsizei width, GLsizei height) // Resize And Initialize The GL Window { if (height==0) // Prevent A Divide By Zero By { height=1; // Making Height Equal One } glViewport(0, 0, width, height); // Reset The Current Viewport
下面几行代码是为屏幕创建视图.意味着物体按大小来区分距离远近.这样可以创建一个现实的观看场景.该视觉是用一个45度角基于窗体的宽高计算所得的.0.1f和100.0f的意思是我们能绘制到屏幕的深度的起始点和结束点.
glMatrixMode(GL_PROJECTION)表示接下来的两行代码是切换到投影矩阵进行处理.投影矩阵是负责添加视觉到我们的场景的.
glLoadIdentity是类似重置的作用.它把当前切换到的矩阵恢复到原始状态.在调用完glLoadIdentity函数之后,我们就开始创建我们场景视图.
glMatrixMode(GL_MODELVIEW)表示任何新的转换都会影响到模型视图矩阵.模型视图矩阵就是我们存放物体信息的容器.最后我们重置模型视图矩阵.暂时不用深究该技术细节,我将会在后面的章节讲解.你目前只需要知道它必须要写来实现视觉场景.
glMatrixMode(GL_PROJECTION); // Select The Projection Matrix glLoadIdentity(); // Reset The Projection Matrix // Calculate The Aspect Ratio Of The Window gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,0.1f,100.0f); glMatrixMode(GL_MODELVIEW); // Select The Modelview Matrix glLoadIdentity(); // Reset The Modelview Matrix }
下面代码会创建OpenGL的环境.我们会设定屏幕背景的颜色,开启深度缓存,开启平滑渐变,等等.该程序会在OpenGL窗体创建的时候被调用.该函数有返回值,但是当前的入门示例并没有那么复杂,所以该返回值可以先不管.
int InitGL(GLvoid) // All Setup For OpenGL Goes Here {
下面这行开启平滑渐变.平滑渐变通过多边形很好的混合颜色和平滑理顺光源.我将会在其它教程解释平滑渐变的细节.
glShadeModel(GL_SMOOTH); // Enables Smooth Shading
下面这行是设置当清空屏幕时的屏幕颜色.如果你不了解颜色怎么用数值表示,我很快就会在后面解释.颜色值的范围是从0.0f到1.0f. 其中0.0f表示最黑(暗),而1.0f是表示最白(亮).glClearColor函数的第一个参数是红色的强度,第二个参数是绿色而第三个是蓝色.这三个值越接近1.0f,对应颜色的光度就越大.最后一个值是透明值.现在只是清空屏幕的时候,我们不需要理会第4个值.就留空在默认值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); // Black Background
下面三行代码是处理深度缓存的.可以把深度缓存想象成屏幕的层次.深度缓存保持跟踪物体在屏幕下的深度.本节的程序暂时还未用到深度缓冲,但所有OpenGL程序在屏幕绘制3D图形时都会用到深度缓存.它用来区分开哪个对象先绘制,例如在圆形后面绘制的正方形不会处于圆形的顶部.深度缓存是OpenGL非常重要的部分.
glClearDepth(1.0f); // Depth Buffer Setup glEnable(GL_DEPTH_TEST); // Enables Depth Testing glDepthFunc(GL_LEQUAL); // The Type Of Depth Test To Do
接着我们要告诉OpenGL我们需要把视角修正设置为最优.这个特性只会消耗极少量的资源,但会让视角画面看起来好点.
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Really Nice Perspective Calculations
最后我们返回TRUE.如果我们想看下初始化是否成功,可以检查返回值是TRUE还是FALSE.如果有错误你可以添加代码到返回FALSE的状态.但现在暂时先不用管这个值.
return TRUE; // Initialization Went OK }
这个函数是专门写绘制代码的地方.所有打算显示到屏幕的物体都是在这里编码.往后的各章节教程多数在这个函数里面加代码.如果你已经学完OpenGL,你就可以在glLoadIdentity函数的return TRUE语句之前创建基础形状.如果你是初学OpenGL,可以接着看后面的教程.当前我们会先做的是用之前的选定的颜色来填满屏幕,清空深度缓存和重置场景.我们暂时先不会绘制任何物体.
返回TRUE是表示程序没问题.如果你希望程序遇到一些状况后退出,可以把返回FALSE添加到异常处理中.这样程序就会退出.
int DrawGLScene(GLvoid) // Here's Where We Do All The Drawing { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear The Screen And The Depth Buffer glLoadIdentity(); // Reset The Current Modelview Matrix return TRUE; // Everything Went OK }
下面函数是在程序退出前调用的.KillGLWindow函数是释放渲染上下文,设备上下文和终止窗体句柄.我会添加一堆错误检测.如果程序不能销毁窗体的任何部件,就会弹出错误消息窗口,来告知你关闭失败.这样可以更容易定位你代码中的问题.
GLvoid KillGLWindow(GLvoid) // Properly Kill The Window {
我们在KillGLWindow函数中做的第一件事是检查我们是否在全屏模式下.如果是在全屏模式下,我们会返回到桌面.我们可以在全屏模式关闭之前销毁窗体,但是这样做的话有些显卡会报错.所以我们还是先关闭全屏模式.这样可以防止桌面报错,并在Nvidia和3dfx显卡都运作正常!
if (fullscreen) // Are We In Fullscreen Mode? {
我们通过ChangeDisplaySettings(NULL,0)语句返回到原来的桌面.传参NULL和0来通知Windows用回Windows注册表中保存的状态值(默认分辨率,位深度,刷新频率等等)来回复到原来的桌面.当我们跳回桌面后就可以恢复显示鼠标了.
ChangeDisplaySettings(NULL,0); // If So Switch Back To The Desktop ShowCursor(TRUE); // Show Mouse Pointer }
下面的代码是检查我们是否有渲染上下文.如果没有创建,会跳到更后面的代码段检查是否有设备上下文.
if (hRC) // Do We Have A Rendering Context? {
如果已经创建渲染上下文,以下代码会检查我们是否可以释放它(从设备上下文中分离出渲染上下文).留意到我们在检查错误.我一直在告诉程序尝试释放它(用下面的语句),然后检查是否释放成功.更便捷的是把操作语句都放进检查语句中.
if (!wglMakeCurrent(NULL,NULL)) // Are We Able To Release The DC And RC Contexts? {
如果我们不能释放设备上下文和渲染上下文,MessageBox函数会弹出错误提示消息.NULL参数的意思是消息窗体没有父窗体.NULL右边的参数是显示在消息窗体的文本."SHUTDOWN ERROR"是现在消息窗体的顶部的文本(标题).MB_OK表示按钮的类型.MB_ICONINFORMATION会在文本旁边显示一个稍微突出的感叹图案.
MessageBox(NULL,"Release Of DC And RC Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION); }
接着我们尝试删除渲染上下文.如果删除失败会弹出错误消息.
if (!wglDeleteContext(hRC)) // Are We Able To Delete The RC? {
如果删除渲染上下文失败,就弹窗提示.然后渲染上下文的变量hRC会被只空值(NULL).
MessageBox(NULL,"Release Rendering Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION); } hRC=NULL; // Set RC To NULL }
现在我们来检查程序是否有设备上下文,而如果有,就释放它.如果我们释放失败,也弹窗提示并把设备上下文变量置空值(NULL).
if (hDC && !ReleaseDC(hWnd,hDC)) // Are We Able To Release The DC { MessageBox(NULL,"Release Device Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION); hDC=NULL; // Set DC To NULL }
现在我们检查是否已有窗体句柄,如果有的话我们会尝试用DestroyWindow(hWnd)语句来销毁该句柄.如果我们销毁窗体失败,也会弹窗提示并把窗体句柄的变量置空值(NULL).
if (hWnd && !DestroyWindow(hWnd)) // Are We Able To Destroy The Window? { MessageBox(NULL,"Could Not Release hWnd.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION); hWnd=NULL; // Set hWnd To NULL }
最后要做的是反注册窗体类.这个允许我们完全的杀死窗体,然后在不会提示"重复注册窗体类"的情况下重新打开一个窗体.
if (!UnregisterClass("OpenGL",hInstance)) // Are We Able To Unregister Class { MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION); hInstance=NULL; // Set hInstance To NULL } }
下面我们开始创建OpenGL窗体.我花了一段时间考虑我是该以简洁的代码创建一个固定的全屏窗体,还是用简便的方案但以复杂代码定义我们的窗体.最后我选择了后者.我一直在问以下问题: 我们怎么用窗体来代替全屏? 我怎么更改窗体标题? 我们怎么更改分辨率和窗体的像素格式? 下面的代码解答了上面几条问题! 所以用后者比较容易学习,也让写OpenGL程序更简单!
如你所见,函数返回布尔值,有5个参数: 窗体标题,窗体宽度,窗体高度,颜色位数(16/24/32),和最后的全屏标记,TRUE是全屏,FALSE是窗体.我们返回一个布尔值会告诉我们窗体是否创建成功.
BOOL CreateGLWindow(char* title, int width, int height, int bits, bool fullscreenflag) {
当我们向Windows设置一个与我们要求匹配的像素格式时,Windows会从我们这个设定值PixelFormat变量中找.
GLuint PixelFormat; // Holds The Results After Searching For A Match
变量wc是用来保存我们的窗体类结构的.窗体类结构体是保存关于我们窗体的信息的.通过更改该类中的不同成员,可以更改窗体的外观和交互效果.每个窗体都属于一个单独的窗体类.在你创建窗体前,你必须先为窗体注册一个类.
WNDCLASS wc; // Windows Class Structure
dwExStyle和dwStyle是分别保存扩展和普通窗体样式信息的.我用了多个变量来保存样式,这样我可以根据我需要创建的窗体类型来控制(全屏的话是弹窗,窗体模式的话是对话框).
DWORD dwExStyle; // Window Extended Style DWORD dwStyle; // Window Style
下面5行代码是定位方形的左上角和右下角的值.我们会用这些值来调整我们的窗体,这样绘制出来的分辨率就会精准了.一般情况下,我们绘制640x480分辨率的窗体时,边框会占用一些像素.
RECT WindowRect; // Grabs Rectangle Upper Left / Lower Right Values WindowRect.left=(long)0; // Set Left Value To 0 WindowRect.right=(long)width; // Set Right Value To Requested Width WindowRect.top=(long)0; // Set Top Value To 0 WindowRect.bottom=(long)height; // Set Bottom Value To Requested Height
下面这行是把局部变量fullscreenflag的值赋给全局变量.
fullscreen=fullscreenflag; // Set The Global Fullscreen Flag
下面代码中,我们为窗体创建一个实例,然后声明窗体类.
CS_HREDRAW和CS_VREDRAW样式会强迫窗体在更改大小的时候重绘.CS_OWNDC为窗体创建一个私有的设备上下文.意味着在程序内部的各个窗体不共享上下文.WndProc变量是程序用来监视消息的函数指针.没有额外的窗体数据,所以我们把额外属性置0.然后我们设置实例.接着设置hIcon属性为空,因为我们暂时不需要窗体图标,顺便把鼠标指针的图标也设置成默认的箭头.背景颜色没关系(因为我们会在OpenGL中另外设置).该窗体中我们不需要菜单,所以设置为空,剩下的窗体类名可以随便给.这里我随便给个"OpenGL"而已.
hInstance = GetModuleHandle(NULL); // Grab An Instance For Our Window wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; // Redraw On Move, And Own DC For Window wc.lpfnWndProc = (WNDPROC) WndProc; // WndProc Handles Messages wc.cbClsExtra = 0; // No Extra Window Data wc.cbWndExtra = 0; // No Extra Window Data wc.hInstance = hInstance; // Set The Instance wc.hIcon = LoadIcon(NULL, IDI_WINLOGO); // Load The Default Icon wc.hCursor = LoadCursor(NULL, IDC_ARROW); // Load The Arrow Pointer wc.hbrBackground = NULL; // No Background Required For GL wc.lpszMenuName = NULL; // We Don't Want A Menu wc.lpszClassName = "OpenGL"; // Set The Class Name
现在我们来注册类.如果中间有任何异常,就会有错误消息弹出.点确定就会退出程序.
if (!RegisterClass(&wc)) // Attempt To Register The Window Class { MessageBox(NULL,"Failed To Register The Window Class.","ERROR",MB_OK|MB_ICONEXCLAMATION); return FALSE; // Exit And Return FALSE }
现在来检查是否全屏.如果用户选择了全屏,我们就进入全屏.
if (fullscreen) // Attempt Fullscreen Mode? {
下面这几行代码有的人会看得云里雾里的,其实是转换到全屏.转换到全屏时有几个重要点要注意的.确保宽高是你想要的,更重要的是,创建窗体前要先设置好全屏模式.这里是把之前设定好的变量赋给窗体而已.
DEVMODE dmScreenSettings; // Device Mode memset(&dmScreenSettings,0,sizeof(dmScreenSettings)); // Makes Sure Memory's Cleared dmScreenSettings.dmSize=sizeof(dmScreenSettings); // Size Of The Devmode Structure dmScreenSettings.dmPelsWidth = width; // Selected Screen Width dmScreenSettings.dmPelsHeight = height; // Selected Screen Height dmScreenSettings.dmBitsPerPel = bits; // Selected Bits Per Pixel dmScreenSettings.dmFields=DM_BITSPERPEL|DM_PELSWIDTH|DM_PELSHEIGHT;
下面我们清空空间来保存视频设置.我们设置需要转换到的宽,高和位.我们在dmScreenSetting中保存所有宽高位的信息.在ChangeDisplaySetting后面尝试转换到储存在dmScreenSetting中的模式.我在转换模式时用CDS_FULLSCREEN变量,因为这样可以去掉屏幕底部的启动栏,加上它在全屏和窗体间切换的时候不会移动和更换你窗体的大小.
// Try To Set Selected Mode And Get Results. NOTE: CDS_FULLSCREEN Gets Rid Of Start Bar. if (ChangeDisplaySettings(&dmScreenSettings,CDS_FULLSCREEN)!=DISP_CHANGE_SUCCESSFUL) {
如果上面的代码不能切换模式,下面的代码就会执行.如果想要的全屏模式不存在,会有弹窗提示两个选项.. 可以选择运行在窗体模式还是直接退出.
// If The Mode Fails, Offer Two Options. Quit Or Run In A Window. if (MessageBox(NULL,"The Requested Fullscreen Mode Is Not Supported By Your Video Card. Use Windowed Mode Instead?","NeHe GL",MB_YESNO|MB_ICONEXCLAMATION)==IDYES) {
如果用户选择了窗体模式,全屏的状态变量会赋FALSE值,然后程序继续运行.
fullscreen=FALSE; // Select Windowed Mode (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; // Exit And Return FALSE } } }
因为上面这段全屏失败并切换到窗体模式的原因,我们要在创建屏幕/窗体类型之前重新检查全屏状态值是TRUE还是FALSE.
if (fullscreen) // Are We Still In Fullscreen Mode? {
如果是仍然处于全屏模式下,就设置额外样式为WS_EX_APPWINDOW,就是一旦窗体可见就把顶级窗体强迫下放到任务栏.而窗体样式就设定为WS_POP.这个窗体类型是没有边框,这样使它能适应全屏模式.
最后,我们会禁用鼠标指针.如果你的程序非交互式的话,全屏模式下禁用鼠标通常是好的.不过视乎你决定.
dwExStyle=WS_EX_APPWINDOW; // Window Extended Style dwStyle=WS_POPUP; // Windows Style ShowCursor(FALSE); // Hide Mouse Pointer } else {
如果用窗体代替全屏模式,我们会添加WS_EX_WINDOWEDGE到扩展样式中.这样可以让窗体看上去更三维.样式上我们会用WS_OVERLAPPEDWINDOW代替WS_POPUP.WS_OVERLAPPEDWINDOW会创建一个有标题栏,可以更改边框,有窗体菜单和有最小化最大化按钮的窗体.
dwExStyle=WS_EX_APPWINDOW | WS_EX_WINDOWEDGE; // Window Extended Style dwStyle=WS_OVERLAPPEDWINDOW; // Windows Style }
下面代码是用来调节我们创建的窗体的样式的.调节后会使窗体精确的确定到我们设定的分辨率.边框会重叠为窗体的部件.用AdjustWindowRectEx命令来确定OpenGL场景没有被边框覆盖,相反,窗体会被扩大到预留空间绘制边框.在全屏模式下,该命令不会影响.
AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle); // Adjust Window To True Requested Size
在下面代码段中,我们会创建窗体并检查是否正确.我们传递所有用到的参数进CreateWindowEx()函数.我们选择要用的扩展样式.类名(就是上面注册窗体类时用的名).窗体标题.窗体样式.窗体左上角位置(0,0是最保险的).窗体的宽高.我们暂时不需要父窗体和菜单,所以我们设置成NULL.传入窗体实例并把最后一个参数置空置.
注意,我们要跟随之前定好的窗体样式,包含 WS_CLIPSIBLINGS和WS_CLIPCHILDREN. WS_CLIPSIBLINGS和WS_CLIPCHILDREN要同时包含来确保OpenGL正常运作.这两个样式防止其它窗体在我们的OpenGL窗体上面或内部绘制图形.
if (!(hWnd=CreateWindowEx( dwExStyle, // Extended Style For The Window "OpenGL", // Class Name title, // Window Title WS_CLIPSIBLINGS | // Required Window Style WS_CLIPCHILDREN | // Required Window Style dwStyle, // Selected Window Style 0, 0, // Window Position WindowRect.right-WindowRect.left, // Calculate Adjusted Window Width WindowRect.bottom-WindowRect.top, // Calculate Adjusted Window Height NULL, // No Parent Window NULL, // No Menu hInstance, // Instance NULL))) // Don't Pass Anything To WM_CREATE
然后我们检查窗体是否创建正常了.如果创建完毕,hWnd会持有窗体句柄.如果不正常,下面代码会弹窗提示错误消息,程序也会跟着退出.
{ KillGLWindow(); // Reset The Display MessageBox(NULL,"Window Creation Error.","ERROR",MB_OK|MB_ICONEXCLAMATION); return FALSE; // Return FALSE }
下面的代码段描述了像素格式.我们挑选了一个支持OpenGL和双缓存的格式,和RGBA一样(红绿蓝,透明管道).我们尝试找一种像素格式匹配我们选定的颜色位数(16bit,24bit,32bit).最后我们创建一个16位的Z-Buffer.剩下的参数要不没用到,要不就是不重要(先别管模板缓存和堆积缓存).
static PIXELFORMATDESCRIPTOR pfd= // pfd Tells Windows How We Want Things To Be { sizeof(PIXELFORMATDESCRIPTOR), // Size Of This Pixel Format Descriptor 1, // Version Number PFD_DRAW_TO_WINDOW | // Format Must Support Window PFD_SUPPORT_OPENGL | // Format Must Support OpenGL PFD_DOUBLEBUFFER, // Must Support Double Buffering PFD_TYPE_RGBA, // Request An RGBA Format bits, // Select Our Color Depth 0, 0, 0, 0, 0, 0, // Color Bits Ignored 0, // No Alpha Buffer 0, // Shift Bit Ignored 0, // No Accumulation Buffer 0, 0, 0, 0, // Accumulation Bits Ignored 16, // 16Bit Z-Buffer (Depth Buffer) 0, // No Stencil Buffer 0, // No Auxiliary Buffer PFD_MAIN_PLANE, // Main Drawing Layer 0, // Reserved 0, 0, 0 // Layer Masks Ignored };
如果创建窗体没报错的话,我们就会获取一个OpenGL设备上下文.如果获取设备上下文失败,就会弹窗提示消息,程序也会退出.
if (!(hDC=GetDC(hWnd))) // Did We Get A Device Context? { KillGLWindow(); // Reset The Display MessageBox(NULL,"Can't Create A GL Device Context.","ERROR",MB_OK|MB_ICONEXCLAMATION); return FALSE; // Return FALSE }
获取到设备上下文后,我们会尝试找一种适合之前描述要求的像素格式.如果窗体找不到匹配的像素格式,会弹窗报错并退出程序.
if (!(PixelFormat=ChoosePixelFormat(hDC,&pfd))) // Did Windows Find A Matching Pixel Format? { KillGLWindow(); // Reset The Display MessageBox(NULL,"Can't Find A Suitable PixelFormat.","ERROR",MB_OK|MB_ICONEXCLAMATION); return FALSE; // Return FALSE }
如果窗体找到匹配的像素格式,我们就会尝试设置像素格式.如果设置不成功,就会弹窗提示错误消息,程序会退出.
if(!SetPixelFormat(hDC,PixelFormat,&pfd)) // Are We Able To Set The Pixel Format? { KillGLWindow(); // Reset The Display MessageBox(NULL,"Can't Set The PixelFormat.","ERROR",MB_OK|MB_ICONEXCLAMATION); return FALSE; // Return FALSE }
如果像素格式设置成功,我们就会尝试获取渲染上下文.如果获取失败就会弹窗报错并退出程序.
if (!(hRC=wglCreateContext(hDC))) // Are We Able To Get A Rendering Context? { KillGLWindow(); // Reset The Display MessageBox(NULL,"Can't Create A GL Rendering Context.","ERROR",MB_OK|MB_ICONEXCLAMATION); return FALSE; // Return FALSE }
如果以上皆通过,我们就会创建设备上下文和渲染上下文,剩下要做的就是激活渲染上下文.如果激活不成功,就弹窗报错并退出程序.
if(!wglMakeCurrent(hDC,hRC)) // Try To Activate The Rendering Context { KillGLWindow(); // Reset The Display MessageBox(NULL,"Can't Activate The GL Rendering Context.","ERROR",MB_OK|MB_ICONEXCLAMATION); return FALSE; // Return FALSE }
如果一切顺利,OpenGL窗体创建成功后,就是显示窗体了,把它设置为前端窗体(给它更多的优先级),然后设置聚焦到该窗体.然后调用ReSizeGLScene函数,传递宽高来设定我们需要的OpenGL屏幕.
ShowWindow(hWnd,SW_SHOW); // Show The Window SetForegroundWindow(hWnd); // Slightly Higher Priority SetFocus(hWnd); // Sets Keyboard Focus To The Window ReSizeGLScene(width, height); // Set Up Our Perspective GL Screen
最后我们调用InitGL()函数,我们自定义用来创建光源,纹理和其它需要创建的属性.你也可以添加自己的错误校验到InitGL函数,然后返回TRUE或FALSE.例如,如果你正在载入纹理的时候遇到错误可以停止程序.如果你返回FALSE,就会弹窗报错并退出程序.
if (!InitGL()) // Initialize Our Newly Created GL Window { KillGLWindow(); // Reset The Display MessageBox(NULL,"Initialization Failed.","ERROR",MB_OK|MB_ICONEXCLAMATION); return FALSE; // Return FALSE }
如果上面的都通过了,就可以确认窗体创建成功了.我们返回TRUE给WinMain()函数告知没出错.这样程序才会继续执行下去.
return TRUE; // Success }
该函数是处理所有窗体消息的地方.当我们注册窗体类时,就会绑定该函数来处理窗体消息.
LRESULT CALLBACK WndProc( HWND hWnd, // Handle For This Window UINT uMsg, // Message For This Window WPARAM wParam, // Additional Message Information LPARAM lParam) // Additional Message Information {
这个代码是把消息值当成状态来判断.uMsg会对应到我们要处理的消息名.
switch (uMsg) // Check For Windows Messages {
如果uMsg变量的值是WM_ACTIVATE,我们就会检查窗体是否仍然在激活状态.如果窗体被最小化该值会是FALSE.如果窗体处于激活状态,该值会是TRUE.
case WM_ACTIVATE: // Watch For Window Activate Message { if (!HIWORD(wParam)) // Check Minimization State { active=TRUE; // Program Is Active } else { active=FALSE; // Program Is No Longer Active } return 0; // Return To The Message Loop }
如果是uMsg的值是WM_SYSCOMMAND(系统命令),我们会对比wParam的值.如果wParam是SC_SCREENSAVE或SC_MONITORPOWER,就代表屏幕保护程序将会启动或者屏幕进入省电模式.我们会返回0以阻止这两种状况发生.
case WM_SYSCOMMAND: // Intercept System Commands { switch (wParam) // Check System Calls { case SC_SCREENSAVE: // Screensaver Trying To Start? case SC_MONITORPOWER: // Monitor Trying To Enter Powersave? return 0; // Prevent From Happening } break; // Exit }
如果uMsg的值是WM_CLOSE,窗体会被关闭.我们会发出一个退出消息,这样主线程中的循环会被中断.done变量会被设置为TRUE,WinMain函数中的主线程循环会停止,程序会退出.
case WM_CLOSE: // Did We Receive A Close Message? { PostQuitMessage(0); // Send A Quit Message return 0; // Jump Back }
如果有键被按下,我们可以通过判断wParam来确定.然后我们把keys数组中对应的值设置成TRUE.之后可以通过读取该数组来找出哪些键被按下了.这样就可以允许判断多键同时按下事件了.
case WM_KEYDOWN: // Is A Key Being Held Down? { keys[wParam] = TRUE; // If So, Mark It As TRUE return 0; // Jump Back }
如果键被松开,我们可以用wParam查键数组获得.然后就把数组中查得的值置FALSE.这样我读到那个数位就能知道键是按着的还是松开的.键盘上的每个键都可以用0-255之间的数值表示.例如,当我按下一个键时,返回了一个40的值,键数组的第40位的值会变成TRUE.到我松开后,它就会变回FALSE.这就是我们用数组位保存按键状态的方式.
case WM_KEYUP: // Has A Key Been Released? { keys[wParam] = FALSE; // If So, Mark It As FALSE return 0; // Jump Back }
当我们改变窗体大小时,就会触发事件并返回消息,uMsg的值会变成WM_SIZE.我们可以通过读取LOWORD和HIWORD的值来获取窗体大小变更后的宽高值.然后把新的宽高值传递进ReSizeGLScene()函数.OpenGL场景就会相应的变更到新的宽高.
case WM_SIZE: // Resize The OpenGL Window { ReSizeGLScene(LOWORD(lParam),HIWORD(lParam)); // LoWord=Width, HiWord=Height return 0; // Jump Back } }
我们暂时不关心的消息可以直接传递到DefWindowProc函数,这样Windows自然会处理它们.
// Pass All Unhandled Messages To DefWindowProc return DefWindowProc(hWnd,uMsg,wParam,lParam); }
这是我们Windows程序的入口点.这里是调用常规函数,处理窗体消息和监测用户交互操作的.
int WINAPI WinMain( HINSTANCE hInstance, // Instance HINSTANCE hPrevInstance, // Previous Instance LPSTR lpCmdLine, // Command Line Parameters int nCmdShow) // Window Show State {
这里设置两个变量.变量msg用来检测当前等待处理的消息.变量out初始值是FALSE.它是用来标记当前还在运行状态.只要它的值仍然是FALSE,程序就会继续运行.如果从FALSE变成TRUE,程序就会退出.
MSG msg; // Windows Message Structure BOOL done=FALSE; // Bool Variable To Exit Loop
这段代码是可有可无的.它弹窗询问是否需要运行在全屏模式.如果用户点击NO按钮,变量fullscreen的值就会从TRUE变成FALSE,程序也会运行在窗体模式.
// Ask The User Which Screen Mode They Prefer if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO) { fullscreen=FALSE; // Windowed Mode }
这里是创建OpenGL窗体的地方.我们传入标题,宽高和色深,还有全屏选择给CreateGLWindow函数.这样就行了!我比较这种简洁的代码.如果窗体因为某些原因失败,这里会返回FALSE,然后程序会终止.
// Create Our OpenGL Window if (!CreateGLWindow("NeHe's OpenGL Framework",640,480,16,fullscreen)) { return 0; // Quit If Window Was Not Created }
这里是循环的起始.只要done变量不为FALSE就会一直循环.
while(!done) // Loop That Runs Until done=TRUE {
循环内部首先要做的是检查是否有等待处理的窗体消息.通过PeekMessage函数,我们可以在不暂停程序的情况下检查消息.有很多程序使用GetMessage()函数代替.它也是一样的作用,但是用GetMessage函数的话,你的程序就会什么都不做,直到收到绘制消息或者其它窗体消息.
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Is There A Message Waiting? {
下面这段代码是检查是否有退出消息发布.如果当前循环收到来自PostQuitMessage(0)函数产生的WM_QUIT消息,变量done就会被设置为TRUE,并促使程序结束.
if (msg.message==WM_QUIT) // Have We Received A Quit Message? { done=TRUE; // If So done=TRUE } else // If Not, Deal With Window Messages {
如果消息不是退出消息,我们就把它转换并派发,这样WndProc()函数或者Windows就可以处理它了.
TranslateMessage(&msg); // Translate The Message DispatchMessage(&msg); // Dispatch The Message } } else // If There Are No Messages {
如果暂时没有消息,我们会绘制OpenGL场景.第一行代码是检查当前窗体是否在激活状态.如果ESC键被按下,变量done就会被设置为TRUE,促使程序结束.
// Draw The Scene. Watch For ESC Key And Quit Messages From DrawGLScene() if (active) // Program Active? { if (keys[VK_ESCAPE]) // Was ESC Pressed? { done=TRUE; // ESC Signalled A Quit } else // Not Time To Quit, Update Screen {
如果程序当前是激活状态,并且ESC键没被按下,我们就提交场景并切换缓存(利用双缓存可以得到平滑无闪烁的动画).利用双缓存,我们可以在隐藏屏幕(后台)绘制所有物体而前端不会见到.当我们切换缓存时,当前屏幕会变成隐藏屏幕,而隐藏的屏幕会变成可视.这样的话我们就会看到场景逐渐绘制出来.因为它是实时出现的.
DrawGLScene(); // Draw The Scene SwapBuffers(hDC); // Swap Buffers (Double Buffering) } }
下面的代码是新加入的(2005年1月).它可以让我们通过按F1在全屏模式和窗体模式之间切换.
if (keys[VK_F1]) // Is F1 Being Pressed? { keys[VK_F1]=FALSE; // If So Make Key FALSE KillGLWindow(); // Kill Our Current Window fullscreen=!fullscreen; // Toggle Fullscreen / Windowed Mode // Recreate Our OpenGL Window if (!CreateGLWindow("NeHe's OpenGL Framework",640,480,16,fullscreen)) { return 0; // Quit If Window Was Not Created } } } }
如果变量done的值变为TRUE,程序就会结束.我们要在关闭OpenGL窗体之前释放资源,然后退出程序.
// Shutdown KillGLWindow(); // Kill The Window return (msg.wParam); // Exit The Program }
在本节中,我尝试解释尽量多的细节,包括所有初始化步骤,例如像创建全屏模式的OpenGL程序,按ESC退出,监控窗体是否激活.我花了2周时间写这节的代码,..(后面省略一大堆话,作者应该是有工匠/艺术家情结的,或者说是完美主义)