zoukankan      html  css  js  c++  java
  • [译]NeHe教程

    原文: 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周时间写这节的代码,..(后面省略一大堆话,作者应该是有工匠/艺术家情结的,或者说是完美主义)

  • 相关阅读:
    第二节:Java环境变量配置
    第一节:VS充当IIS的配置步骤(VS2017和VS2019)
    .Net进阶系列(21)-跨域请求
    02-Unity深入浅出(二)
    第十五节:Expression表达式目录树(与委托的区别、自行拼接、总结几类实例间的拷贝)
    01-Unity深入浅出(一)
    第十二节:SQLServer存储过程详解及EFCore调用
    Android 组件系列-----Activity的传值和回传值
    Access大数据高效分页语句
    C#清除HTML样式
  • 原文地址:https://www.cnblogs.com/live41/p/3415184.html
Copyright © 2011-2022 走看看