NEHE的OpenGL-Lesson 01 Setting Up OpenGL in Windows

来源:互联网 发布:艺术字logo设计软件 编辑:程序博客网 时间:2024/05/18 12:40

 HeHe OpenGL Tutorial 是相当不错教程,也是目前我见过的最好的 OpenGL 教程。教程的每一篇都是结合着实例, 结合着源代码来讲解的, 循序渐进,是学习 OpenGL 的佳品。 尤其是作者认真的态度和他的耐心更是值得赞叹,他会详细地讲解每一行代码的实际作用。

 

注意, 本教程只适用于对3D原理、 OpenGL基础和 Win32编程有着一定了解的人,因为关于这些基础理论的细节已经超出了本教程的范围。 对于任何学习 OpenGL 的人来说, OpenGL红皮书是必不可少的,它更像是一本你需要经常翻阅的工具书, 它会让你了解到 OpenGL 的各种细节问题。

 

我会以同样认真的态度来翻译好此教程, 翻译后的每一句话我都会反复阅读一两遍以上。因为我也是一位 OpenGL 新手, 再加上工作量很大,所以教程中有可能存在一些问题或者疏漏。 如果你发现了这些请即时反馈给我, 这样我就能做出相应的补救或者更正。十分欢迎你的支持和鼓励, 那将会使我更有动力。

 

感谢gameres.com 的支持,在gameres.com 和我的个人主页 AmanPage.com 上你都能找到这个教程以及它的更新。 另外,在 gameres.com 的论坛开设了专门的板块,欢迎到那里与大家一起讨论!

 

 

 

(原文保留字)

Originally compiled by LOneWoolf

Recompiled by M0RPhEuS

 

The lessons on this page may containmistakes, poor commenting, and should not be considered the best resource to learnOpenGL from. What you do with the code is up to you. I am merely trying to makethe learning process a little easier for those people new to OpenGL. If you areserious about learning OpenGL, you should spend the money and invest in theOpenGL Red Book (ISBN 0-201-46138-2) and OpenGL Blue Book (ISBN 0-201-46140-4).I have the second edition of each book, and although they can be difficult forthe new OpenGL programmer to understand, they are by far the best books writtenon the subject of OpenGL. Another book I would recommend is the OpenGLSuperbible, although opinions vary. It is also important that you have a solidunderstanding of the language you plan to use. Although I do comment the non-GLlines, I am self-taught, and may not always write proper or even good code.It's up to you to take what you have learned from this site and apply it toprojects of your own. Play around with the code, read books, ask me questionsif need be. Once you have surpassed the code on this site or even before, checkout some of the more professional sites such as OpenGL.org. Also be sure tovisit the many OpenGL links on my page. Each site I link to is an incredibleasset the OpenGL community. Most of these sites are run by talented individualsthat not only know their GL, they also program alot better than I do. Pleasekeep all of this in mind while browsing my site. I hope you enjoy what I haveto offer, and hope to see projects created by yourself in the near future!

 

One final note, if you see code that youfeel is to similar to someone else's code, please contact me. I assure you, anycode I borrow from or learn from either comes from the MSDN or from sitescreated to help teach people in a similar way that my site teaches GL. I neverintentionally take code, and never would without giving the proper personcredit. There may be instances where I get code from a free site not knowingthat site took it from someone else, so if that happens, please contact me. Iwill either rewrite the code, or remove it from my program. Most the codeshould be original however, I only borrow when I absolutely have no idea how toaccomplish something, and even then I make sure I understand the code before Idecide to include it in my program. If you spot mistakes in any of the lessons,no matter how tiny the mistake may be, please let me know.

 

One important thing to note about my basecode is that it was written in 1997. It has undergone many changes, and it isdefinitely not borrowed from any other sites. It will more than likely be alteredin the future. If I am not the one that modifies it, the person responsible forthe changes will be credited.


Lesson 01 Setting Up OpenGL in Windows

 

In this tutorial, I will teach you how to set up, and use OpenGL in a Windows environment. The program you create in this tutorial will display an empty OpenGL window, switch the computer into fullscreen or windowed mode, and wait for you to press ESC or close the Window to exit. It doesn't sound like much, but this program will be the framework for every other tutorial I release in the next while.  

It's very important to understand how OpenGL works, what goes into creating an OpenGL Window, and how to write simple easy to understand code. You can download the code at the end of the tutorial, but I definitely recommend you read over the tutorial at least once, before you start programming in OpenGL.

 

 

欢迎使用我的 OpenGL 教程。我是一个对 OpenGL 充满热情的人! 我第一次听说 OpenGL 还是 3Dfx 发布 Voodoo 1 卡的 OpenGL 硬件加速驱动的时候, 当时我就意识到 OpenGL 是我必须要学习的东西。 不幸的是当时很难从书本或者网络上找到关于 OpenGL 的信息。 我花了很多时间来调试代码、在 email 和 IRC(在线聊天系统) 中恳求他人的帮助,但我发现那些懂得 OpenGL 的人保留了他们的精华,也没有兴趣共享他们的知识。 这实在令人灰心。

 

我创建这个网站是为了帮助那些有兴趣学习OpenGL 而又需要帮助的人。 在每一篇教程中我都会尽可能详细地解释每一行代码的作用。我会保持代码的简洁(无需MFC代码)。就算是Visual C++ 和 OpenGL 的新手也应该能读懂代码并且清楚地知道发生了什么。 对于 OpenGL 的高级程序员来说这个站点可能太简单了, 但是如果你刚刚起步,我想这个站点将会给你许多帮助!

 

这篇教程已经在2000年一月份彻底重写了。这篇教程将会教给你如何设置一个 OpenGL 视窗,它可以是窗口模式的或者全屏幕模式的, 任和你想要的尺寸, 分辨率 和色彩深度。 这里的代码很灵活并且可以运用在你所有的OpenGL 工程中, 它也是这个教程的基础代码!这里的代码灵活而强大, 所有错误都被报告, 没有内存泄露, 易读并且容易修改。感谢 Fredric Echols 对代码所做的修改。

 

让我们从代码开始。 首先, 我们需要在Visual C++ 中创建一个工程。 假如你不知道怎么做的话,我想你应该先学习 Visual C++ 而不是 OpenGL。 可供下载的代码是用 Visual C++ 6.0 写成的。 在某些版本的 VC++ 中, 需要把 bool 改为 BOOL, true 改为 TRUE, false 改为 FALSE。做了上述修改之后, 代码可以在 Visual C++4.0和5.0 中编译通过。

 

在Visual C++ 中当你创建了一个新的 Win32 程序(并非控制台程序) 之后, 你还需要链接 OpenGL 的库文件。 操作步骤是: Project-> Settings, 点击 LINK 标签, 在 “Object/Library Modules” 下面那一行的开始处(在kernel32.lib之前) 增添 OpenGL32.lib,GLu32.lib 和 GLaux.lib,完成之后点击 OK 按钮。 现在你已经准备好来书写一个 OpenGL 视窗程序了。

 

代码的前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 程序中被沿用。

 

下面第一行代码设置一个 RenderingContext (渲染描述表,着色描述表,渲染上下文),每一个 OpenGL 程序都会连接一个Rendering Context, Rendering Context 将 OpenGL调用连接到 Device Context (设备描述表,设备上下文)。OpenGL RenderingContext 在这里被定义为 hRC。要在窗口中进行绘制我们还需要创建一个 Device Context,这是第二行代码所做的事情, Windows Device Context 在这里被定义为hDC。 DC 将窗口连接到 GDI (Graphics Device Interface, 图形设备接口), 而 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 可以反映出我们的程序是否运行于全屏幕模式,是的话它就为 TRUE; 如果运行于窗口模式,它就为 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() 的函数体定义却又在CreateGLWindow() 的后面。 我们知道在 C 语言中要使用一个函数的话函数必须在之前已经声明了, 所以这里我们要先声明WndProc()。

 

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);                 // Declaration For WndProc

 

下一段代码的作用是当窗口大小被调整时 (对于窗口模式) 调整 OpenGL 场景的大小。如果窗口大小并不能被调整 (例如全屏幕模式的时候), 此段代码也至少会在程序首次运行的时候被调用一次, 用来设置透视。 OpenGL 场景会根据它的显示窗口的宽与高来做调整。

 

GLvoid ReSizeGLScene(GLsizeiwidth, 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 的所有设置,比如设置清除屏幕所用的颜色, 打开深度缓存(DepthBuffer), 打开平滑阴影等。 这段程序在 OpenGL 窗口创建完成之前不会被调用。 这段程序具有返回值,但由于现在我们的初始化工作并不复杂, 所以现在可以不必关心这个返回值。

 

int InitGL(GLvoid)                                                                // All Setup For OpenGL Goes Here

{

下面这行代码用于打开平滑阴影。 平滑阴影会在多边形内精细地混合颜色,和平滑光照。 关于平滑阴影的更多细节会在以后的教程中阐述。

 

       glShadeModel(GL_SMOOTH);                                       // Enables Smooth Shading

 

接下来的这行代码用于设置清除屏幕所用的颜色。 如果你不知道颜色是怎样工作的(译注:在OpenGL中), 我会快速地解释一下。 颜色值的范围是从 0.0f 到 1.0f, 0.0f 最暗,而 1.0f最亮。 第一个参数是 红颜色 的亮度,然后第二个参数是 绿颜色 的亮度, 第三个参数是蓝颜色 的亮度。 指定的颜色值越接近 1.0f 颜色就会越明亮。第四个参数是 Alpha 值, 在此我们对其不必不关心,现在将其设置为 0.0f。 我会在以后的教程中进行解释。

 

我们通过混合 红、绿、蓝 三个原色来得到不同的色彩,这些你在学校的时候应该学习过。 所以当使用 glClearColor(0.0f, 0.0f, 1.0f,0.0f) 时我们清除屏幕颜色为亮蓝色,glClearColor(0.5f,0.0f, 0.0f, 0.0f) 清除屏幕颜色为 中红色。 不是最亮,也不是最暗的。 要产生白色的背景(译注:即清除屏幕色), 就需要尽可能地把颜色值设的高(1.0f), 要产生黑色背景就要尽可能地把颜色值设的低(0.0f)。

 

       glClearColor(0.0f, 0.0f, 0.0f, 0.0f);                                // Black Background

 

 

此下的三行代码用于设置深度缓存(DepthBuffer), 你可以把深度缓存想象为屏幕里的一个层。 深度缓存会记录下物体在屏幕中的深度。在这个程序中我们并不真的使用深度缓存, 但是几乎所有的 OpenGL 三维程序都会使用深度缓存。深度缓存会拣选出哪一个物体需要被先绘制, 这样如果你在一个圆形的后面绘制一个正方形, 那么正方形不会覆盖到圆形的上面来。在 OpenGL 中深度缓存是十分重要的。

 

译注: 深度缓存又称 ”Z-缓存”。原著言 “深度缓存会拣选出哪一个物体需要被先绘制” 其实是一种比喻。深度缓存实际上存储了屏幕中每个像素点的深度值, 这样具有较小深度值的像素 (距离我们近的像素) 会把具有较大深度值的像素 (距离我们远的像素) 覆盖掉。

 

       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。 但是现在我们不必关心它。

 

       return TRUE;                                                  // Initialization Went OK

}

 

下面的这段将是所有绘图代码出现的位置, 所有你想绘制的东西都将出现在这段代码中。在之后的教程中, 会向此段代码中添加更多的内容。 如果你已经对 OpenGL 有了一些了解, 你可以试着绘制一些基本图形并把代码添加到glLoadIdentity() 之后, return TRUE 之前。 如果你是个 OpenGL 新手, 请等待之后的教程。现在, 所有我们做的就是 清除屏幕到之前指定的颜色,清除深度缓存 和 重置场景 (译注:就是重置当前视图模型矩阵)。 我们并不绘制任何东西。

 

代码 return TRUE 告知我们的程序运行无问题。 如果由于一些原因你想停止程序, 那么在 return TRUE 之前的某处增加 returnFALSE, 它将告知程序 绘图代码 失败,然后程序将会退出。

 

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()的工作就是释放 Rendering Context,Device Context 还有 窗口句柄。 我已经加入了很多错误检查,如果在摧毁窗口过程中出现了任何错误, 都会弹出一个消息对话框来通知你出现了什么错误。 这使得检查程序错误的工作变得容易一些。

 

GLvoid KillGLWindow(GLvoid)                                              // Properly Kill The Window

{

 

在KillGLWindow() 中首先要做的就是检查我们是否处于全屏幕模式, 是的话, 我们需要先切换回桌面。如果我们先 摧毁窗口 而后 消除全屏幕模式的话, 在某些显卡中可能会使桌面崩溃。 所以这里我们先消除全屏幕模式,后摧毁窗后。 这在 Nvidia 和3dfx 显卡上都能正常工作。

 

       if (fullscreen)                                                         // Are We In Fullscreen Mode?

       {

 

我们使用 ChangeDisplaySettings(NULL,0)切换回原来的桌面。 输入 NULL 和 0 强制 Windows 使用注册表中的值 (默认的分辨率, 颜色深度, 刷新率等) 来有效地恢复原来的桌面。 然后, 我们恢复了鼠标指针的可见。

 

              ChangeDisplaySettings(NULL,0);                   // If So Switch Back To The Desktop

              ShowCursor(TRUE);                                       // Show Mouse Pointer

       }

 

然后检查我们是否有一个 RenderingContext (hRC)。

 

       if (hRC)                                                           //Do We Have A Rendering Context?

       {

 

如果我们拥有一个 RenderingContext, 下面的代码将检查我们能否释放它 (将hRC从hDC分开)。 请注意我检查错误的方法是很巧妙的。

 

              if (!wglMakeCurrent(NULL,NULL))    // Are We Able To Release The DC And RC Contexts?

              {

 

如果不能释放 DC 和RC contexts, MessageBox() 将会弹出一条DC 和RC 不能被释放的消息。参数 NULL 表示消息窗口没有父窗口,然后的参数分别是 窗口消息 和 窗口标题。最后的参数中的 MB_OK 表示我们要一个带有 OK 按钮的窗口, MB_ICONINFORMATION表示要在消息窗口中绘制一个带圆圈的小写 i 的图标。

 

                     MessageBox(NULL,"Release Of DC And RC Failed.","SHUTDOWNERROR",MB_OK | MB_ICONINFORMATION);

              }

 

接下来试着释放Rendering Context,如果失败了会有消息弹出。

 

              if (!wglDeleteContext(hRC))                                  // Are We Able To Delete The RC?

              {

 

如果不能释放Rendering Context 的话将弹出RC释放失败的消息, 随后设置hRC为 NULL。

 

                     MessageBox(NULL,"Release Rendering ContextFailed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);

              }

              hRC=NULL;                                             // Set RC To NULL

       }

 

然后检查我们是否拥有Device Context,有的话将其释放。不能释放的话同样会弹出消息, 随后设置hDC为NULL。

 

       if (hDC && !ReleaseDC(hWnd,hDC))                                   // Are We Able To Release The DC

       {

              MessageBox(NULL,"Release Device ContextFailed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);

              hDC=NULL;                                             // Set DC To NULL

       }

 

同样的方式,释放窗口句柄。

 

       if (hWnd && !DestroyWindow(hWnd))           // Are We Able ToDestroy The Window?

       {

              MessageBox(NULL,"Could Not ReleasehWnd.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);

              hWnd=NULL;                                                 // Set hWnd To NULL

       }

 

最后要做的是注销窗口类。 这允许我们完整地摧毁窗口,而在之后重新打开其它窗口时不会收到 "Windows Class alreadyregistered" 这样的错误信息。

 

       if (!UnregisterClass("OpenGL",hInstance))                         // Are We Able To Unregister Class

       {

              MessageBox(NULL,"Could Not UnregisterClass.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);

              hInstance=NULL;                                           // Set hInstance To NULL

       }

}

 

 

下一段代码用于创建一个 OpenGL 窗口。我花了很多时间来做决定, 是否要制作一个可以节省大量的代码的仅支持全屏幕模式的窗口, 还是制作一个需要大量代码但可以被用户方便定制的更友好的窗口。我想后者是最好的选择。 我经常收到一些 e-mail 问此类的问题:我怎样才能创建一个不使用全屏幕模式的窗口?我怎样才能改变窗口标题? 我怎样才能改变窗口的分辨率(译注:指全屏幕模式) 和像素格式?  接下来的代码会完成这一切!  所以这是更好的学习材料, 以后书写你自己的 OpenGL 程序就会容易很多了。

 

你可以看见, 这个函数返回 BOOL 变量 (TRUE 或 FLASE), 并且接收 5个参数: 窗口标题, 窗口宽度,窗口高度, 色彩深度 (16/24/32)  还有 全屏幕模式开关。TRUE 代表使用全屏幕模式, FALSE代表使用窗口模式。 函数返回一个 BOOL 值来表明窗口创建是否成功。

 

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, 这样我们在摧毁窗口的时候就可以正确地决定是否要返回桌面,否则就会出问题。

 

       fullscreen=fullscreenflag;                               // Set The Global Fullscreen Flag

 

 

下一段代码, 我们获取我们的应用程序实例句柄,然后定义窗口类。

 

窗口风格 CS_HREDRAW 和 CS_VREDRAW 强制窗口在调整尺寸时重绘。 CS_OWNDC用于创建一个私有的窗口 DC, 也就是说这个DC 并不能跨程序共享。 WndProc是窗口过程, 不需要额外的窗口数据于是我们把接下来的2个参数设置为 0。 然后我们设置应用程序句柄, 接下来我们设置了默认的窗口图标和鼠标箭头。之后的窗口背景色对于我们来说无关紧要(我们在OpenGL中设置其)。 我们不需要菜单所以将其设为NULL, 之后的类名可以随便设置, 为了简单我设为“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

 

 

现在我们注册窗口类。 如果出了什么错误, 就会弹出消息框,点击 OK 按钮就会退出。

 

       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?

       {

 

下面的代码好像对于一些人来说有一大堆的问题……就是切换到全屏幕模式。 在切换到全屏幕模式的时候,有一些重要的事情你得记住。 比如应该确保你在全屏幕模式下使用的窗口的宽和高应该和你在窗口模式中使用的宽和高相同,更重要的, 应该在创建窗口之前设置全屏幕模式。 在这个代码中, 你无须担心宽和高的问题,全屏模式和窗口模式的宽可高都将被设置为请求的值。

 

译注:我并不担心全屏和窗口的尺寸一致问题, 也不担心设置全屏和创建窗口的先后问题,我担心的是不要设置了一个显示器不支持的分辨率模式, 例如100x100全屏模式, 那样你永远不会成功

 

              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;

 

 

在上面的代码中, 我们清除了用于保存 显示设置参数的空间, 然后设置了需要的宽, 高 和色彩深度。  在下面的代码中,我们将尝试设置请求的全屏幕模式。 我们的关于宽,高 和色彩深度 的信息都保存在dmScreenSettings 中, 下面代码中的 ChangeDisplaySettings 将尝试设置与dmScreenSettings中相匹配的模式。我使用了 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 NotSupported By\nYour Video Card. Use Windowed Mode Instead?","NeHeGL",MB_YESNO|MB_ICONEXCLAMATION)==IDYES)

                     {

 

如果用户选择使用窗口模式, 变量fullscreen 将被设置为 FALSE,程序继续运行。

 

                            fullscreen=FALSE;                            // Select Windowed Mode (Fullscreen=FALSE)

                     }

                     else

                     {

 

如果选择了退出, 那么又会弹出一个消息框告诉用户程序即将结束。然后代码段返回 FALSE 值, 程序获知窗口创建失败,程序退出。

 

                            // Pop Up A Message Box Letting User Know The ProgramIs Closing.

                            MessageBox(NULL,"ProgramWill Now Close.","ERROR",MB_OK|MB_ICONSTOP);

                            return FALSE;                                   // Exit And Return FALSE

                     }

              }

       }

 

 

由于上面的代码在 切换模式的时候有可能失败并转为 窗口模式继续运行, 所以我们需要再次检查 fullscreen 。

 

       if (fullscreen)                                                  // Are We Still In Fullscreen Mode?

       {

 

如果我们仍在全屏设置中, 我们设置扩展风格为WS_EX_APPWINDOW, 这可以强制使我们的窗口可见于最前面。对于通常风格我们将其设置为WS_POPUP,没有边框的弹出式窗口, 使得全屏模式窗口完美显示。

 

最后, 禁止鼠标箭头。 如果你的程序没有鼠标交互的话在全屏幕模式下这是一个好主意。

 

              dwExStyle=WS_EX_APPWINDOW;                              // Window Extended Style

              dwStyle=WS_POPUP;                                                  // Windows Style

              ShowCursor(FALSE);                                                    // Hide Mouse Pointer

       }

       else

       {

 

如果使用窗口模式替代了全屏模式, 我们将增加WS_EX_WINDOWEDGE到扩展风格, 这可以使窗口具有 3D 外观。 而对于通常风格, 我们用WS_OVERLAPPEDWINDOW 代替WS_POPUP,这是具有标题栏, 窗口菜单(译注:指左上角的菜单), 最大化最小化按钮 和 可调整尺寸的窗口。

 

              dwExStyle=WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;                   // Window Extended Style

              dwStyle=WS_OVERLAPPEDWINDOW;                                               //Windows Style

       }

 

下面的这行代码会根据我们的窗口风格来调整我们的窗口尺寸。 当然,为了得到精确的绘图区尺寸, 窗口模式的窗口会变得大一些, 那是边框占据的位置。对于全屏幕模式来说这条函数没有效果。

 

       AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle);         // Adjust Window To True Requested Size

 

下一段代码我们将创建窗口并且检查是否成功。 作为参数,我们使用了扩展窗口风格, 类名(必须与之前注册的类名相同), 窗口标题, 通常样式, 窗口左上角坐标,窗口宽与高。 我们没有父窗口也不需要菜单, 所以两个都设为 NULL。 然后是应用程序句柄, 最后一个参数输入 NULL。

 

注意我们的通常窗口风格还连带了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 AdjustedWindow 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 CreationError.","ERROR",MB_OK|MB_ICONEXCLAMATION);

              return FALSE;                                                        // Return FALSE

       }

 

下一段代码用于描述象素点格式。 我们选择了一个支持 OpenGL, 支持双缓冲 和 RGBA 模式的格式。 然后试着找出一个与我们要求的色彩深度相匹配色深(16bit,24bit,32bit), 最后我们设置一个16bit的Z-缓存。其余的参数都不需要或者不重要 (除了stencil buffer (模版缓存) 和accumulation buffer (积累缓存))。

 

       static      PIXELFORMATDESCRIPTORpfd=              // pfd Tells Windows How We Want Things To Be

       {

              sizeof(PIXELFORMATDESCRIPTOR),                // SizeOf 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 Device Context。 如果不能获取 DC 的话那么将会弹出一个错误消息框, 然后程序退出(返回 FALSE)。

 

       if (!(hDC=GetDC(hWnd)))                                             // Did We Get A Device Context?

       {

              KillGLWindow();                                                     // Reset The Display

              MessageBox(NULL, "Can't Create A GL DeviceContext.", "ERROR", MB_OK|MB_ICONEXCLAMATION);

              return FALSE;                                                               // Return FALSE

       }

 

 

如果我们为 OpenGL 窗口成功的获取了 DC, 那么接下来我们将尝试找出一个与我们上面所描述的相匹配的像素点格式。如果 Windows 未找到一个相匹配的像素点格式,那么照例将弹出消息框然后程序退出(返回 FALSE)。

 

       if (!(PixelFormat=ChoosePixelFormat(hDC,&pfd)))                            // Did Windows Find A Matching Pixel Format?

       {

              KillGLWindow();                                                                   // Reset TheDisplay

              MessageBox(NULL, "Can't Find A SuitablePixelFormat.", "ERROR",MB_OK|MB_ICONEXCLAMATION);

              return FALSE;                                                                             // Return FALSE

       }

 

如果找到了一个相匹配的像素点格式, 我们将尝试设置像素点格式。如果不能设置像素点格式, 还是会弹出消息框, 然后程序退出(返回 FALSE)。

 

       if(!SetPixelFormat(hDC,PixelFormat,&pfd))                          // Are We Able To Set The Pixel Format?

       {

              KillGLWindow();                                                            // Reset The Display

              MessageBox(NULL,"Can't Set ThePixelFormat.","ERROR",MB_OK|MB_ICONEXCLAMATION);

              return FALSE;                                                                      // Return FALSE

       }

 

如果像素点格式被成功地设置了, 那么接下来我们将尝试获取一个Rendering Context。 获取失败的话会弹出消息框,然后程序退出。

 

       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

       }

 

 

如果迄今为止一切顺利, 并且我们也成功地创建了Device Context 和 RenderingContext 的话, 那么现在我们该做的就是将Rendering Context 激活。 如果激活失败了会弹出消息框,然后程序退出(返回 FALSE)。

 

       if(!wglMakeCurrent(hDC,hRC))                                     // Try To Activate The Rendering Context

       {

              KillGLWindow();                                                     // Reset The Display

              MessageBox(NULL, "Can't Activate The GL RenderingContext.", "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(代表出现错误)。 例如, 你在InitGL() 中载入纹理并遇到了错误, 你可以要求程序停止。如果InitGL() 返回了 FALSE 那么下面的代码将会接收到并且弹出错误消息, 然后程序退出。

 

       if (!InitGL())                                                    // Initialize Our Newly Created GL Window

       {

              KillGLWindow();                                       // Reset The Display

              MessageBox(NULL,"InitializationFailed.","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_ACTIVE, 那么我们要检查我们的窗口是否还处于活动状态。如果窗口被最小化了, 那么变量active 的值将为 FLASE; 如果窗口是活动的, 那么active 的值将为 TRUE。

 

              case WM_ACTIVATE:                                      // Watch ForWindow 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

              }

 

如果消息是WM_SYSCOMMAND (系统消息) 那么我们得再次比较wParam 的值。如果wParam为 SC_SCREENSAVE 或SC_MONITORPOWER, 那么分别代表 屏幕保护程序将启动和 显示器将进入节能模式。 我们返回 0 以阻止这种事情的发生。

 

              case WM_SYSCOMMAND:                        //Intercept System Commands

              {

                     switch (wParam)                              // CheckSystem Calls

                     {

                            case SC_SCREENSAVE:             // Screensaver Trying To Start?

                            caseSC_MONITORPOWER:              //Monitor Trying To Enter Powersave?

                            return 0;                                          // Prevent From Happening

                     }

                     break;                                              //Exit

              }

 

 

如果uMsg 为WM_CLOSE 表示窗口被关闭。 那么我们发出一条退出消息,然后变量 done (译注:在 WinMain 中)会被设置为 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 找到它,数组keys[] 中与那个键相对应的元素将被设置为 FALSE。  通过这种方法, 我们可以知道键的按下与抬起,键盘上的每个键的值都在 0-255 之间。比如我按下了一个键值为 40 的键, keys[40] 会被设置为 TRUE;当我松开了这个键, keys[40] 会被设置为 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 消息。 我们通过读取lParam 的低字和高字的值可以获取新的窗口尺寸的宽与高。 然后我们以新的尺寸值作为参数调用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,                         // PreviousInstance

                     LPSTR            lpCmdLine,                         // Command LineParameters

                     int          nCmdShow)                       // Window Show State

{

 

我们设置两个变量: msg 用于检测是否有消息需要处理; done 开始被设置为 TRUE,意思是我们的程序还没有完成运行。 它的值保持 FLASE 的话程序就会继续运行,当它变为 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 FullscreenMode?", "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)

       {

              fullscreen=FALSE;                                          // Windowed Mode

       }

 

 

下面是创建 OpenGL 窗口的代码。我们输入标题, 宽与高, 颜色深度, 和 TRUE(全屏) 或 FALSE(窗口) 给CreateGLWindow。 就这么简单, 我很高兴能保持代码的简洁。如果由于一些原因窗口创建失败了, 它会返回 FALSE 然后我们的程序会终止(返回0)。

 

       // Create Our OpenGL Window

       if (!CreateGLWindow("NeHe's OpenGLFramework",640,480,16,fullscreen))

       {

              return 0;                                                 // Quit If Window Was Not Created

       }

 

这是我们的主循环, 只要 done 保持为 FALSE 它就会一直重复下去。

 

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

       {

 

 

首先,我们要看看是否有 Windows 消息待处理。使用PeekMessage() 检索消息可以避免程序等待。很多程序使用GetMessage(),它虽然工作的很好, 但是它会一直等待直到有消息出现, 这期间我们的程序做不了任何事情。

 

              if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))                 // IsThere 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;                       // ESCSignalled A Quit

                            }

                            else                                    // Not Time To Quit, Update Screen

                            {

 

 

如果程序处于活动状态并且 ESC 按钮没有被按下,我们将绘制场景然后交换缓冲区 (使用双缓冲技术我们可以得到平滑无闪烁的动画)。 使用双缓冲技术, 物体将被绘制到一个我们不可见的屏幕上(译注:指后缓冲区)。当交换缓冲区时, 我们原来可见的屏幕变得不可见, 我们原来不可见的屏幕变得可见。(译注:交换前后缓冲区) 使用这种方法,在场景绘制完毕之前我们看不见它, 绘制完毕后会一下子显示出来。(译注:计算机飞快地绘制然后交换前后帧,快速重复此过程式就得到了平滑的动画)

 

 

                                   DrawGLScene();                        // Draw The Scene

                                   SwapBuffers(hDC);                   // Swap Buffers (Double Buffering)

                            }

                     }

 

 

下一段代码是后来加的, 功能是允许我们使用 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 OpenGLFramework",640,480,16,fullscreen))

                            {

                                   return 0;                                          // Quit If Window Was Not Created

                            }

                     }

              }

       }

 

如果变量 done 不再是 FALSE, 那么程序退出。 我们会完全地摧毁窗口,所有东西都会被释放掉。

 

       // Shutdown

       KillGLWindow();                                                     // Kill The Window

       return (msg.wParam);                                                 // Exit The Program

}

 

 

 

在这一篇教程中我尽量做了详细的解释, 设置的每一个步骤,创建你自己的全屏幕 OpenGL 程序,按下 ESC键会退出的功能, 以及监视程序的活动状态等等这些事情。我花了大约两周的时间来写代码, 一周的时间来修改 bug 并同大师交流,两天时间 (大约22个小时) 来写这个 HTML 文件。 如果你有什么意见或者问题请给我写 email。 如果你发现有什么不正确的地方或者是你有更好的办法, 请告诉我,我想尽我可能地做最好的 OpenGL 教程,而且我有兴趣听取你的反馈。

 

Jeff Molofee (NeHe)

 

(译著) 我花了大约 3 天的时间来翻译这一篇教程,原著说“我会尽可能详细地解释每一行代码的作用”, 他果然说到做到。 翻译过程绝对不是一个单纯的过程,要经过反复的思考与推敲。 我会以同样认真的态度来翻译好这个教程。反馈对我同样重要, 我在乎文中的每一个字。如果你发现了什么疏漏, 请即时通知我, 或者, 也十分欢迎你的支持和鼓励,我会很乐意阅读你的来信。

原创粉丝点击