[译]NeHe教程 - 创建一个OpenGL窗体

时间:2022-02-11 04:46:38

原文: 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 <gl\gl.h> // Header File For The OpenGL32 Library
#include <gl\glu.h> // Header File For The GLu32 Library
#include <gl\glaux.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[];                              // 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==) // Prevent A Divide By Zero By
{
height=; // Making Height Equal One
} glViewport(, , 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,);          // 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); // Set Left Value To 0
WindowRect.right=(long)width; // Set Right Value To Requested Width
WindowRect.top=(long); // 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 = ; // No Extra Window Data
wc.cbWndExtra = ; // 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,,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\nYour 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
, , // 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
, // 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
, , , , , , // Color Bits Ignored
, // No Alpha Buffer
, // Shift Bit Ignored
, // No Accumulation Buffer
, , , , // Accumulation Bits Ignored
, // 16Bit Z-Buffer (Depth Buffer)
, // No Stencil Buffer
, // No Auxiliary Buffer
PFD_MAIN_PLANE, // Main Drawing Layer
, // Reserved
, , // 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 ; // 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 ; // Prevent From Happening
}
break; // Exit
}

如果uMsg的值是WM_CLOSE,窗体会被关闭.我们会发出一个退出消息,这样主线程中的循环会被中断.done变量会被设置为TRUE,WinMain函数中的主线程循环会停止,程序会退出.

case WM_CLOSE:                          // Did We Receive A Close Message?
{
PostQuitMessage(); // Send A Quit Message
return ; // Jump Back
}

如果有键被按下,我们可以通过判断wParam来确定.然后我们把keys数组中对应的值设置成TRUE.之后可以通过读取该数组来找出哪些键被按下了.这样就可以允许判断多键同时按下事件了.

case WM_KEYDOWN:                        // Is A Key Being Held Down?
{
keys[wParam] = TRUE; // If So, Mark It As TRUE
return ; // 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 ; // 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 ; // 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",,,,fullscreen))
{
return ; // Quit If Window Was Not Created
}

这里是循环的起始.只要done变量不为FALSE就会一直循环.

while(!done)                                // Loop That Runs Until done=TRUE
{

循环内部首先要做的是检查是否有等待处理的窗体消息.通过PeekMessage函数,我们可以在不暂停程序的情况下检查消息.有很多程序使用GetMessage()函数代替.它也是一样的作用,但是用GetMessage函数的话,你的程序就会什么都不做,直到收到绘制消息或者其它窗体消息.

if (PeekMessage(&msg,NULL,,,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",,,,fullscreen))
{
return ; // Quit If Window Was Not Created
}
}
}
}

如果变量done的值变为TRUE,程序就会结束.我们要在关闭OpenGL窗体之前释放资源,然后退出程序.

    // Shutdown
KillGLWindow(); // Kill The Window
return (msg.wParam); // Exit The Program
}

在本节中,我尝试解释尽量多的细节,包括所有初始化步骤,例如像创建全屏模式的OpenGL程序,按ESC退出,监控窗体是否激活.我花了2周时间写这节的代码,..(后面省略一大堆话,作者应该是有工匠/艺术家情结的,或者说是完美主义)