NeHe OpenGL教程第一课 创建一个OpenGL窗口(Wiin32)

(以下内容为我个人按照NeHe英文教程原文,以及一些中文资料做的翻译,由于自己的英语水平有限,可能有些地方翻译的不是很正确,欢迎指正,共同提高,希望以下文档能够给你一些帮助。

注:以下代码均是在Visual c++ 6.0下编写的,如果希望代码成功编译通过,请在Visual c++ 6.0下调试代码。

以下代码已在Visual c++ 6.0下成功编译运行。如果在编译过程中出现错误,请仔细检查是否某处书写不正确。或者到NeHe的Tutorial网站中下载所需代码。

网址:http://nehe.gamedev.net/tutorial/creating_an_opengl_window_(win32)/13001/

以下所有资料的版权为NeHe所有,如需转载或使用,请标注源码出处。)

在这个教程里,我将教你在Windows环境中创建一个OpenGL程序。它显示一个空的OpenGL窗口,可以在窗口和全屏模式下切换,按ESC退出。它是我们以后应用程序的框架。


在这里我将教你如何设置一个OpenGL窗口。它可以是一个窗口,或者是全屏,它可以是你想要的任意大小,任意的分辨率,任意的色彩深度。这部分代码是很灵活的,可以在你的所有的OpenGL工程中使用。所有的教程都是基于这部分代码的。这部分代码不仅很灵活,而且健壮性很好。所有的错误都可以提出。(All errors are reported.原文,可能我翻译的不是很准确。)这些代码没有内存泄漏的问题,而且这些代码非常容易阅读和修改。

现在我们就直接从代码开始吧。首先,你应该在Visual C++中建立一个工程。如果你不知道怎么做,那你因该先学习如何使用Visual C++,然后再学习OpenGL。可以下载的代码是Visual C++ 6.0的代码。一些Visual C++的版本需要使用BOOL代替bool,使用TRUE代替true,使用FALSE代替false。按照上面提到的做相应的替换,你可以在Visual C++ 4.0 和Visual C++ 5.0中成功地编译代码。

在Visual C++ 6.0中建立一个新的Win32 Application (而不是一个 console application) 之后,你需要链接OpenGL的libraries。在Visual C++ 6.0中,选择Project->Setting,然后点击LINK选项卡。在"Object/Library Modules" 选项的最开始(在kernel32.lib之前),添加OpenGL32.lib 、GLu32.lib 和 GLaux.lib.。然后点击OK。现在你可以准备编写一个OpenGL窗口程序了。


注 #1:很多的编译器没有定义CDS_FULLSCREEN。如果你的程序遇到一个和CDS_FULLSCREEN有关的错误,你需要在你的程序的上面添加 #define CDS_FULLSCREEN 4这段代码。


注 #2:过去写的教程中,GLAUX是可以使用的。现在不支持GLAUX了。这个网站上的很多教程还在使用旧的GLAUX代码。如果你的编译器不支持GLAUX或者你不怎么想使用它,在主页中下载 GLAUX REPLACEMENT CODE 。

 

代码的前四行包含了我们使用的库的头文件。如下所示:

#include <windows.h>  // Windows头文件
#include <gl\gl.h>    // OpenGL32库头文件
#include <gl\glu.h>   // GLu32库头文件
#include <gl\glaux.h> // GLaux库头文件
接下来你需要定义所有你想要在你的程序中使用的变量。这个程序只是创建一个空白的OpenGL窗口,所以我们现在不需要设置太多的变量。我们下面定义的这几个变量是非常重要的,在你的所有的OpenGL程序中都会使用到。

第一行定义了一个渲染用到的上下文(Rendering Context)。每一个OpenGL程序都会链接到这个渲染上下文。一个渲染上下文就是把一个OpenGL调用链接到一个设备上下文(Device Context)。渲染上下文定义为hRC变量。你的程序需要定义一个设备上下文来画一个窗口,第二行定义了这样一个变量。这个Windows设备上下文定义为hDC。hDC变量把Window连接到GDI(Graphics Device Interface)。hRC把OpenGL连接到hDC。

第三行的变量hWnd通过Windows系统获得分配到我们的OpenGL窗口的句柄,最后,在第四行的变量创建了一个我们程序的实例。

HGLRC      hRC=NULL;         // 永久的渲染上下文( Rendering Context)
HDC                hDC=NULL;         // 私有的GDI设备上下文( GDI Device Context)
HWND              hWnd=NULL;        // 获得我们窗口的句柄
HINSTANCE   hInstance;        // 获得应用程序的实例
下面的第一行定义了一个用来监视键盘点击事件的数组。有很多的方法可以获得键盘的点击事件,我使用这种方法。这种方法很可靠,它可以同时处理多个点击事件。

active变量用来通知我们的程序我们的OpenGL窗口是否已经最小化到任务栏了。如果我们的OpenGL已经最小化了,我们可以做从暂停到退出程序的任何事情。我比较喜欢暂停程序。在这种情况下,当我们的OpenGL窗口最小化时,我们的程序不会在后台运行。

fullscreen变量相当的明显。(感觉翻译不太通顺,原文:The variable fullscreen is fairly obvious. )。如果我们的程序运行在全屏模式下,fullscreen变量为TRUE,如果我的程序运行在窗口模式下,fullscreen变量为FALSE。定义这个全局变量非常重要,它使得每一个步骤都知道程序是否运行在全屏模式下。

bool   keys[256];              // 用于键盘行为的数组
bool    active=TRUE;            // 窗口活动标记,默认设置为TRUE
bool   fullscreen=TRUE;         // 全屏标记,默认设置为全屏
现在我们要声明 WndProc()函数。之所以这么做是因为CreateGLWindow() 函数需要调用WndProc() 函数,但是WndProc() 函数是在CreateGLWindow()函数之后定义的。在C语言中如果我们想要访问一个出现在当前代码段之后的程序或者是一段代码,我们必须在我们的程序开始对这部分代码进行声明。 所以下面的这行声明WndProc() 函数的代码使得CreateGLWindow() 函数能够访问 WndProc()函数。
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);//声明WndProc()函数
下面这部分代码的作用是无论何时窗口(假设你使用窗口模式而不是全屏模式)的尺寸改变时重新设置你的OpenGL场景的尺寸。即使你不能改变你的窗口的大小(例如,你运行在全屏模式下),这段代码在你第一次运行程序来设置你的透视视图(原文:perspective view)时至少也会被调用一次。OpenGL场景会依据你的显示窗口的宽和高来重新设置它的尺寸。
GLvoid ReSizeGLScene(GLsizei width, GLsizei height)// 初始化和设置OpenGL窗口
{
    if(height==0)                          // 防止被0除
    {
        height=1;                          // 把height设置为1
    }
 
    glViewport(0, 0, width, height);        // 重新设置当前的视图窗口
glMatrixMode(GL_PROJECTION) 下面的代码把屏幕设置为透视视图。意味着远处的物体要比近处的物体小。它创建了一个更逼真的场景。根据窗口的宽和高,用45度的视角来计算透视。0.1f和100.0f是我们可以在屏幕上绘制的深度的起始点和终止点。

glMatrixMode(GL_PROJECTION)函数表示下面的两行代码将会影响投影矩阵(projection matrix)。投影矩阵负责添加透视到我们的场景中。glLoadIdentity()函数可以简单地完成重置功能。它重新存储我们选择的矩阵到它的原始状态。调用glLoadIdentity()函数之后,我们把透视视图添加到我们的场景中。glMatrixMode(GL_MODELVIEW)函数表示任何新的改变都将影响模型视图矩阵。模型视图矩阵是我们的对象信息保存的地方。最后,我们重置了模型视图矩阵。如果你不理解这些也没关系。在后面的教程中将会对这些做出解释。只需要知道如果你想得到一个更好的透视场景,这些工作是必需的。

    glMatrixMode(GL_PROJECTION); //选择投影矩阵

    glLoadIdentity();   //重置投影矩阵
 
    //计算窗口的比率
    gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,0.1f,100.0f);
    glMatrixMode(GL_MODELVIEW); //选择模型视图矩阵
    glLoadIdentity();    //重设模型视图矩阵
}

所有有关OpenGL的设置由下面的这部分代码完成。我们设置用什么颜色来清空屏幕,打开深度缓冲区,启用平滑阴影(原文: smooth shading)等等。这段程序直到OpenGL窗口创建时才会调用。这段程序会有一个返回值,但是因为我们的初始化并不复杂,所以暂且不用考虑这个返回值。

int InitGL(GLvoid)//在这里做所有有关OpenGL的设置
{
下面这行代码用来启用平滑阴影。平滑阴影能够在一个多边形上出色地实现颜色过度,和光照过度。(原文:Smooth shading blends colors nicely across a polygon, and smoothes out lighting.)我们将在后面的教程中对这部分内容做更详细的解释。
glShadeModel(GL_SMOOTH);                       //启用平滑阴影
接下来的代码设置了清屏的颜色。如果你不是很了解这些颜色有什么用处,我会很快做出解释。这些颜色值的范围是从0.0f到1.0f,从最暗到最亮。glClearColor函数的第一个参数是红色的强度,第二个参数是绿色的强度,第三个参数是蓝色的强度。这个值越接近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)。如果你想要一个白色的背景,你应该把所有的颜色强度尽可能地设置为更接近它的最高值(1.0f)。如果你想要一个黑色背景,你应该把所有的颜色强度尽可能地设置为更接近它的最低值(0.0f)。

glClearColor(0.0f, 0.0f, 0.0f, 0.5f);   //黑色背景
接下来的三行代码是为了处理深度缓存。可以把深度缓存想象为屏幕里面的层。深度缓存能跟踪物体在屏幕里的深度。我们在这一个教程中不会用到深度缓存,但是几乎每一个在屏幕上绘制3D图形的OpenGL程序都会用到深度缓存。它用来排列哪一个对象先绘制,这样你就不会把一个圆形后面的一个正方形绘制到圆形上来。深度缓存是OpenGL中非常重要的组成部分。
glClearDepth(1.0f);    //设置深度缓存
glEnable(GL_DEPTH_TEST);  //启用深度测试
glDepthFunc(GL_LEQUAL); //深度测试的类型
下面我们通知OpenGL,我们想要进行最好的透视修正。这会或多或少地影响一些性能,但是会让透视视图看起来更好一点。
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);   //出色的透视计算
最后,函数返回TRUE。如果我们想测试是否初始化正常进行了,我们可以检查函数返回的是TRUE还是FALSE。如果有错误发生,你可以自己添加返回FALSE的代码。现在我们不需要考虑这个。

return TRUE; //初始化成功

}
下一段是你所有的绘图代码部分。任何你想要在屏幕上显示出来的效果都在这部分代码里实现。后面的教程会在程序的这部分加入一些新的代码。如果你已经理解了OpenGL的相关知识,在可以尝试在glLoadIdentity()函数和return TURE语句之间加入一些代码来绘制一些基本的图形。如果你还是一个OpenGL初学者,可以在以后的教程中完成这些。现在我们将用我们开始设置的颜色来清屏,清除深度缓存,然后重新设置场景。我们暂时不会绘制任何图形。

return TRUE语句通知我们的程序这里没有错误。如果你因为某种原因想要程序停止,在return TRUE语句之前的某个位置添加一个return FALSE语句,通知我们的程序绘制过程失败。程序这时会退出。

int DrawGLScene(GLvoid)  //我们绘制图形的地方
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除颜色和深度缓存
    glLoadIdentity(); //重置模型视图矩阵
    return TRUE;   // 所有的代码正确执行
}

下面这部分代码是在程序退出之前调用的。KillGLWindow()函数的作用是释放渲染上下文,设备上下文,最后释放窗口句柄。我加入了很多的错误检查。如果程序不能销毁窗口的任何部分,一个错误信息的消息对话框将会弹出,告诉你哪里出错了。它使得在你的代码中查找错误更加容易。
GLvoid KillGLWindow(GLvoid)   //彻底关闭窗口
{
KillGLWindow()函数中首先要做的是检查我们的窗口是否为全屏模式。如果是,我们将切换回桌面。我们应该在禁用全屏模式前销毁窗口,但是在某些显卡上如果我们在禁用全屏模式前销毁窗口,桌面会崩溃。所以我们首先禁用全屏模式。这样可以防止桌面崩溃,而且在Nvidia和3dfx显卡上效果很好。
if(fullscreen)    //判断是否为全屏模式
{
我们使用 ChangeDisplaySettings(NULL,0)函数返回原始桌面。传递NULL作为第一个参数,0作为第二个参数强制windows系统使用当前存储在windows注册表中的值(默认的分辨率,色彩深度,刷新率等等)有效地恢复到原始桌面。我们切换回原始桌面后,显示鼠标。

    ChangeDisplaySettings(NULL,0);  //如果切换回桌面

    ShowCursor(TRUE);   //显示鼠标
}
下面的代码检查是我们是否获得了一个渲染上下文(hRC)。如果我们还没有获得,程序会跳转到下面检查我们是否获得了一个设备上下文的代码段。
if(hRC)  //我们是否获得了一个渲染上下文
{
如果我们已经获得了渲染上下文,下面的代码会检查是否我们可以释放它(将hRC从hDC分开),注意,我检查错误的方式。基本上我只是通知程序尝试去释放它(利用wglMakeCurrent(NULL,NULL)函数),然后我检查是否释放了。非常完美地将几行代码合并成一行。
if(!wglMakeCurrent(NULL,NULL))    //是否我们可以释放渲染和设备上下文
{
如果我们不能释放渲染和设备上下文,会有一个错误消息的消息框弹出,通知我们渲染和设备上下文不能释放。NULL意味着消息框没有父窗口。NULL右边的文字将在消息框中显示。"SHUTDOWN ERROR"是消息框标题栏的文字。MB_OK意味着我们希望消息框有一个"OK"按钮。MB_ICONINFORMATION是让消息框里显示一个带圆圈的小写的i(看上去更正式一些)。

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

}
然后我们尝试删除渲染上下文。如果不成功,一个错误消息框将会弹出。
if(!wglDeleteContext(hRC))  //我们能否删除渲染上下文
{
如果我们不能删除渲染上下文,下面的代码会弹出一个错误消息框通知我们删除渲染上下文失败。hRC将被设置为NULL。

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

    }
    hRC=NULL;   // hRC设置为NULL
}
然后我们检查是否我们的程序已经获得了一个设备上下文,如果获得了,我们尝试去释放它。如果我们不能释放设备上下文,一个错误消息框将会弹出hDC将被设置为NULL。
if(hDC && !ReleaseDC(hWnd,hDC))  //是否能够释放hDC
{
    MessageBox(NULL,"Release Device Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
    hDC=NULL;  //hDC设置为NULL
}
然后我们检查是否有一个窗口句柄,如果有,我们尝试用DestroyWindow(hWnd)函数来销毁窗口。如果我们不能销毁窗口,一个错误消息框将会弹出,hWnd将被设置为NULL。
if(hWnd && !DestroyWindow(hWnd))   //是否能够销毁窗口
{
    MessageBox(NULL,"Could Not Release hWnd.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
    hWnd=NULL;    //hWnd设置为NULL
}
最后,取消窗口类的注册信息。这使得我们可以正确地杀死窗口,接着打开其他窗口时不会出现"Windows Class already registered"的错误消息框。

if(!UnregisterClass("OpenGL",hInstance))   //能否取消注册信息

    {
        MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
        hInstance=NULL;    // hInstance设置为NULL
    }
}

下面的代码是创建我们的OpenGL窗口。我花了很长时间考虑是否应该创建一个不需要很多额外代码的固定的全屏窗口。最后我还是决定用更多的代码来创建一个用户友好的窗口,这应该是最好的选择。我一直在邮件里询问下面的问题:我怎样创建一个不使用全屏模式的窗口?我应该怎样改变窗口的标题?我应该怎么样改变窗口的分辨率或者是像素格式?下面的代码做了这些工作。因此,最好学习一些材质方面的知识,这对于你写一个自己的OpenGL程序会更加容易些。

这个函数返回一个BOOL(TRUE or FALSE)类型值,它有5个形参:窗口的标题,窗口的宽度,窗口的高度,颜色深度(原文:bits (16/24/32)),最后是一个全屏标识,TRUE为全屏模式,FALSE为窗口模式。我们用一个布尔类型的返回值来通知我们窗口是否创建成功。

BOOL CreateGLWindow(char* title, int width, int height, int bits, bool fullscreenflag)
{

当我们要求Windows为我们寻找相匹配的象素格式时,Windows寻找结束后将模式值保存在变量PixelFormat中。

GLuint      PixelFormat;     // 保存搜索到的匹配值

变量wc用来获得窗口类的结构。窗口类结构中保存了我们窗口的信息。通过在类里面改变不同的字段,我们可以改变窗口的外观和行为。所有的窗口都属于窗口类。在你创建一个窗口之前,你必须为窗口注册一个窗口类。 

WNDCLASS    wc;       // 窗口类结构

dwExStyle 变量和dwStyle变量用来保存扩展的和正常的窗口风格信息。我使用这些变量来保存风格信息,以便于我可以根据我想要创建的窗口类型来改变窗口风格(是全屏的弹出窗口,还是一个带边框的窗口模式的窗口)。

DWORD       dwExStyle;      // 窗口扩展风格

DWORD       dwStyle;        // 普通窗口风格

下面的5行代码用来捕获一个矩形的左上角和右下角的坐标值。我们将使用这些值来调整我们的窗口,以便于我们的绘图区域正好是我们想要的正确的分辨率。通常情况下如果我们创建了一个分辨率为640X480的窗口,窗口的边框会占用其中的一些分辨率。

RECT WindowRect;                            // 获得窗口的左上角/右下角坐标值

WindowRect.left=(long)0;                        // 将Left设置为0

WindowRect.right=(long)width;                   // 将Right设置为需要的宽度值

WindowRect.top=(long)0;                         // 将Top设置为0
WindowRect.bottom=(long)height;                     // 将Bottom设置为需要的高度值

接下来的这行代码我们把全局变量fullscreen的值设置为等于fullscreenflag的值。(如果我们希望在全屏幕下运行而
将fullscreenflag设为TRUE,但没有让变量fullscreen等于fullscreenflag的话,fullscreen变量将保持为FALSE。当我们在全屏幕模式下销毁窗口的时候,变量fullscreen的值却不是正确的TRUE值,计算机将误以为已经处于桌面模式而无法切换回桌面。就是一句话,fullscreen的值必须永远fullscreenflag的值,否则就会有问题。)

fullscreen=fullscreenflag;                      // 设置全局变量fullscreen

下面的这个代码段,我们获得了一个我们窗口的实例,然后我们声明窗口类。CS_HREDRAW 和 CS_VREDRAW风格会在窗口的尺寸方法变化时强制重新绘制窗口。CS_OWNDC 为窗口创建一个私有的DC。意思是说DC在程序中不是共享的。WndProc 是在我们的程序中的消息处理程序。没有额外的窗口数据可用,所以我们把这两个字段设置为0。然后我们设置实例。接下来我们把 hIcon 设置为NULL,我们不想在窗口中有一个ICON,我们使用标准的鼠标箭头。我们不需要关心背景颜色(我们在GL中设置了)。我们在窗口中不想要一个菜单,所以我们把它设置为NULL,你可以为这个类设置任何名字。为了简便,我使用"OpenGL"。

hInstance       = GetModuleHandle(NULL);            // 获得我们窗口的实例

wc.style   = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;  // 移动时重画窗口,并取得窗口的DC

wc.lpfnWndProc      = (WNDPROC) WndProc;     // WndProc 处理消息

wc.cbClsExtra       = 0;        // 没有额外的窗口信息

wc.cbWndExtra       = 0;        // 没有额外的窗口信息

wc.hInstance        = hInstance;     // 设置实例

wc.hIcon        = LoadIcon(NULL, IDI_WINLOGO);          // 加载默认的ICON

wc.hCursor      = LoadCursor(NULL, IDC_ARROW);          // 加载鼠标箭头

wc.hbrBackground    = NULL;        // 不需要为GL设置背景颜色

wc.lpszMenuName     = NULL;       // 不需要菜单

wc.lpszClassName    = "OpenGL";      // 设置类名

现在我们注册类。如果出现错误,一个错误消息框会弹出。点击消息框的OK按钮退出程序。

if (!RegisterClass(&wc))                        // 尝试注册窗口类

{
    MessageBox(NULL,"Failed To Register The Window Class.","ERROR",MB_OK|MB_ICONEXCLAMATION);
    return FALSE;                           // 退出,返回FALSE

}

现在我们检查程序是应该运行在全屏模式还是窗口模式。如果应该运行在全屏模式,我们尝试去设置全屏模式。

if (fullscreen)                             // 尝试设置全屏模式

{

下一部分用来切换到全屏模式的代码很多人会有很多疑问。这里有一些你应该牢记的用来切换到全屏模式的重要的东西。确认你在全屏模式下使用的宽和高和你想要的窗口的一样,更重要的是在你创建你的窗口之前设置全屏模式。在这部分代码中,你不需要担心宽和高,全屏和窗口模式都会设置为需要的尺寸。

DEVMODE dmScreenSettings;           // 设备模式

memset(&dmScreenSettings,0,sizeof(dmScreenSettings));       // 确保内存清空为零

dmScreenSettings.dmSize=sizeof(dmScreenSettings);       // 设备模式结构的尺寸

dmScreenSettings.dmPelsWidth    = width;        // 选择屏幕宽带

dmScreenSettings.dmPelsHeight   = height;      // 选择屏幕高度

dmScreenSettings.dmBitsPerPel   = bits;      // 选择每个像素的色彩深度(原文:Selected Bits Per Pixel )dmScreenSettings.dmFields=DM_BITSPERPEL|DM_PELSWIDTH|DM_PELSHEIGHT;

上面的代码我们存储了我们的显示设置。我们设置了屏幕的宽,高和色彩深度。下面的这部分代码我们尝试去设置想要的全屏模式。我们把所有的关于宽,高和色彩深度的信息存储在dmScreenSettings变量中。在下面的ChangeDisplaySettings 这行代码中,我们尝试切换到我们存储在dmScreenSettings变量中的模式。当切换模式时,我用到了CDS_FULLSCREEN 参数,因为这样做不仅移去了屏幕底部的状态条,而且它在来回切换时,没有移动或改变您在桌面上的窗口。

// 尝试去设置选择的模式然后得到结果。注:CDS_FULLSCREEN 用来移除状态条
if (ChangeDisplaySettings(&dmScreenSettings,CDS_FULLSCREEN)!=DISP_CHANGE_SUCCESSFUL)
{

如果模式不能设置,下面的代码会执行。如果没有可匹配的全屏模式,一个有两个选项的消息框会弹出。一个选项是在窗口模式下运行,一个选项是退出。

// 如果设置模式失败,提供两个选项,退出或者运行在窗口模式。
if (MessageBox(NULL,"The Requested Fullscreen Mode Is Not Supported By\nYour Video Card. Use Windowed Mode Instead?","NeHe GL",MB_YESNO|MB_ICONEXCLAMATION)==IDYES)
{

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

    fullscreen=FALSE;      // 选择窗口模式 (Fullscreen=FALSE)
}
else
{

如果用户决定退出程序,一个提示用户程序将要关闭的消息框弹出。返回FALSE通知我们的程序窗口创建失败。然后程序退出。

 //弹出一个提示用户程序将要关闭的消息框           

 MessageBox(NULL,"Program Will Now Close.","ERROR",MB_OK|MB_ICONSTOP);
            return FALSE;                   // 退出,返回FALSE

        }
    }
}

因为上面的全屏模式可能失败,用户可能决定在窗口模式下运行程序,在我们设置屏幕/窗口类型前再检查一次我们的程序是运行在全屏模式下,还是窗口模式下。

if (fullscreen)                             // 是否为全屏模式

{

如果运行在全屏模式下,设置扩展风格为WS_EX_APPWINDOW,一旦我们的窗口可见,所有屏幕上的窗口都会强制最小化到任务栏。对于窗口风格,我们将创建一个WS_POPUP 风格的窗口。这种类型的窗口没有边框,在全屏模式下效果很好。

最后,我们禁用鼠标箭头。如果我们的程序没有交互,在全屏模式下禁用鼠标效果很好。这由你决定。

    dwExStyle=WS_EX_APPWINDOW;                  // 窗口扩展风格

    dwStyle=WS_POPUP;                       // 窗口风格

    ShowCursor(FALSE);                      // 隐藏鼠标箭头

}
else
{

如果是在窗口模式下,增加WS_EX_WINDOWEDGE 参数到扩展模式。这将增强窗口的3D效果。窗口风格我们使用WS_OVERLAPPEDWINDOW 代替WS_POPUP。WS_OVERLAPPEDWINDOW 风格的窗口带有标题栏、带尺寸的边框、菜单和最大化/最小化按钮。

    dwExStyle=WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;           // 窗口扩展风格    dwStyle=WS_OVERLAPPEDWINDOW;      // 窗口风格

}

下面的代码会根据创建的窗口的风格来调整窗口。这些调整会使我们的窗口处于正确的分辨率。通常边框会占用一部分窗体。通过AdjustWindowRectEx 函数所有OpenGL的场景不会被边框覆盖,相反,窗口会变得更大以便绘制边框。在全屏模式下,这条命令不起作用。

AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle);     // 调整窗口到正确的尺寸

在下面的代码中,我们将创建我们的窗口,然后检查它的创建是否正确。我们给CreateWindowEx() 函数传递所有需要的参数。我们决定使用扩展风格;类名(应该和你注册的窗口类名保持一致);窗口标题;窗口风格;窗口的左上角坐标(0,0 是个安全的选择);窗口的宽和高;我们不需要一个父窗口;我们不想要菜单,所以把这两个参数设置为NULL;传递窗口实例;最后,把最后一个参数设置为NULL。

注意我们包含了WS_CLIPSIBLINGS 和 WS_CLIPCHILDREN 风格在我们的窗口风格中。WS_CLIPSIBLINGS 和 WS_CLIPCHILDREN对于OpenGL的正确运行都是必须的。这些风格防止其他的窗口在我们的OpenGL窗口上面和里面绘图。

if (!(hWnd=CreateWindowEx(  dwExStyle,              // 扩展风格

                "OpenGL",               // 类名

                title,                  // 窗口标题

                WS_CLIPSIBLINGS |           // 窗口风格

                WS_CLIPCHILDREN |           // 窗口风格

                dwStyle,                // 选择窗口风格

                0, 0,                   // 窗口的左上角坐标

                WindowRect.right-WindowRect.left,   // 窗口的宽

                WindowRect.bottom-WindowRect.top,   // 窗口的高             

                NULL,                   // 不需要一个父窗口

                NULL,                   // 没有菜单

                hInstance,              // 窗口实例 
                NULL)))                 // WM_CREATE为NULL

然后我们检查是否我们的窗口是否正确地创建了。如果我们的窗口创建成功,hWnd 获得窗口的句柄。如果窗口没有创建成功,下面的代码会弹出一个错误消息框,然后程序退出。

{
    KillGLWindow();                         // 重置显示区域

    MessageBox(NULL,"Window Creation Error.","ERROR",MB_OK|MB_ICONEXCLAMATION);
    return FALSE;                           // 返回 FALSE
}

下面这部分代码描述了一个像素格式。我们选择了通过RGBA(红、绿、蓝、alpha通道)支持OpenGL和双缓存的格式。我们试图找到匹配我们选定的色彩深度(16位、24位、32位)的象素格式。最后我们设置16位的Z-Buffer。剩下的参数或者没有使用,或者不太重要(模板缓存和累积缓存除外。原文:aside from the stencil buffer and the (slow) accumulation buffer)。

static  PIXELFORMATDESCRIPTOR pfd=  // 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 |       // 格式必须支持OpenGL(Format Must Support OpenGL )

    PFD_DOUBLEBUFFER,        // 必须支持双重缓存(Must Support Double Buffering )

    PFD_TYPE_RGBA,        // 一个RGBA格式(Request An RGBA Format )

    bits,                 // 选择我们的色彩位(Select Our Color Depth )

    0, 0, 0, 0, 0, 0,        // 忽略的色彩位(Color Bits Ignored )

    0,              // 没有Alpha缓存(No Alpha Buffer )

    0,          // 忽略切换色彩位   (Shift Bit Ignored )

    0,            // 没有累积缓存(No Accumulation Buffer )

    0, 0, 0, 0,     // 忽略累积色彩位(Accumulation Bits Ignored )

    16,         // 16位Z-Buffer (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设备上下文。如果我们不能获得一个设备上下文,一个错误消息框将弹出,然后程序退出(返回FALSE)。

if (!(hDC=GetDC(hWnd)))      // 能否获得设备上下文

{
    KillGLWindow();         // 重置显示区域

    MessageBox(NULL,"Can't Create A GL Device Context.","ERROR",MB_OK|MB_ICONEXCLAMATION);
    return FALSE;          // 返回FALSE
}

如果我们成功获得了一个OpenGL窗口的设备上下文,我们将尝试获得一个和上面描述的匹配的像素格式。如果Windows系统无法获得一个匹配的像素格式,一个错误消息框将弹出,然后程序退出(返回FALSE)。

if (!(PixelFormat=ChoosePixelFormat(hDC,&pfd)))             // Windows系统能够获得匹配的像素格式

{
    KillGLWindow();          // 重置显示区域

    MessageBox(NULL,"Can't Find A Suitable PixelFormat.","ERROR",MB_OK|MB_ICONEXCLAMATION);
    return FALSE;        // 返回FALSE
}

如果Windows系统找到了一个匹配的像素格式,我们将尝试去设置这个像素格式。如果这个像素格式不能设置,一个错误消息框将弹出,然后程序退出(返回FALSE)。

if(!SetPixelFormat(hDC,PixelFormat,&pfd))        // 我们能否设置像素格式
{
    KillGLWindow();       // 重置显示区域

    MessageBox(NULL,"Can't Set The PixelFormat.","ERROR",MB_OK|MB_ICONEXCLAMATION);
    return FALSE;        // 返回FALSE
}

如果像素格式正确地设置了,我们尝试去获得一个渲染上下文。如果我们不能获得一个渲染上下文,一个错误消息框将弹出,然后程序退出(返回FALSE)。

if (!(hRC=wglCreateContext(hDC)))         // 我们能否获得渲染上下文
{
    KillGLWindow();         // 重置显示区域

    MessageBox(NULL,"Can't Create A GL Rendering Context.","ERROR",MB_OK|MB_ICONEXCLAMATION);
    return FALSE;        // 返回FALSE
}

如果到目前为止还没有发生错误,我们已经成功地获得了设备上下文和渲染上下文,现在我们要激活渲染上下文。如果我们不能激活渲染上下文,一个错误消息框将弹出,然后程序退出(返回FALSE)。

if(!wglMakeCurrent(hDC,hRC))       // 尝试激活渲染上下文
{
    KillGLWindow();         // 重置显示区域 
    MessageBox(NULL,"Can't Activate The GL Rendering Context.","ERROR",MB_OK|MB_ICONEXCLAMATION);
    return FALSE;        // 返回FALSE
}

如果所有的事情都很顺利,我们的OpenGL窗口成功地创建了,然后我们将要显示窗口。我们将通过传递屏幕的宽和高到ReSizeGLScene函数来设置我们的OpenGL屏幕透视。

ShowWindow(hWnd,SW_SHOW);      // 显示窗口
SetForegroundWindow(hWnd);        // 稍微地提高点优先级
 
SetFocus(hWnd);        // 设置键盘的焦点至此窗口
ReSizeGLScene(width, height);     // 设置透视GL屏幕

最后我们跳到InitGL() 函数,在这里设置光照,纹理和其他需要设置的所有的东西。你可以在InitGL() 函数中设置自己的错误检测,返回TRUE(一切顺利)或者FALSE(出现错误)。例如,如果你在InitGL() 函数中加载纹理出错,你可能想让程序停止。如果你在InitGL() 函数中返回FALSE,下面的代码将把FALSE当成一个错误信息,程序将退出。

if (!InitGL())        // 初始化我们新创建的GL窗口
{
    KillGLWindow();    // 重置显示区域

    MessageBox(NULL,"Initialization Failed.","ERROR",MB_OK|MB_ICONEXCLAMATION);
    return FALSE;        // 返回 FALSE
}

如果至此没有发生过错误,可以推定创建窗口已经成功了。我们向WinMain()函数返回TRUE,通知WinMain()函数没有发生错误。以防止程序退出。

    return TRUE;                                // 创建成功
}

这里是所有的窗口信息处理的地方。当我们注册完窗口类之后,我们通知它跳转到这部分代码来处理窗口信息。

LRESULT CALLBACK WndProc(   HWND    hWnd,                   // 窗口的句柄 
                UINT    uMsg,                   // 窗口信息 
                WPARAM  wParam,                 // 额外的窗口信息

                LPARAM  lParam)                 // 额外的窗口信息
{

下面的代码把uMsg 变量的值和所有的case 声明比较。uMsg 变量保存了我们想要处理的消息的名字。

switch (uMsg)      // 检查窗口信息
{

如果uMsg 变量等于WM_ACTIVATE ,我们检查我们的窗口是否处于活动状态。如果我们的窗口已经最小化了,active变量设置为FALSE。如果我们的窗口还处于活动状态,active变量为TRUE。

case WM_ACTIVATE:         // 监视窗口活动信息
{
    if (!HIWORD(wParam))        // 检查最小化状态 
    {
        active=TRUE;       // 程序处于活动状态 
    }
    else
    {
        active=FALSE;                   // 程序处于非活动状态 
    }
 
    return 0;                       // 返回消息循环
}

如果uMsg 变量等于WM_SYSCOMMAND (系统命令),我们将再次把wParam和case 声明进行比较。如果wParam 等于SC_SCREENSAVE 或 SC_MONITORPOWER的话,不是有屏幕保护要运行,就是显示器想进入节电模式。返回0可以阻止这两件事发生。

case WM_SYSCOMMAND:      // 系统中断命令
{
    switch (wParam)     // 检查系统调用 
    {
        case SC_SCREENSAVE:        // 屏幕保护将要启动 
        case SC_MONITORPOWER:      // 显示器将要进入省电模式 
        return 0;                   // 阻止这两件事发生

    }
    break;        // 退出
}

如果uMsg 等于WM_CLOSE ,窗口将要关闭。我们发送一个退出信息,主循环将会中断。done变量设置为TRUE,WinMain() 函数中的主循环停止,程序将要关闭。

case WM_CLOSE:       // 我们是否接受到了一个退出信息
{
    PostQuitMessage(0);       // 发送一个退出信息 
    return 0;       // 返回
}

如果有一个按键被点击,我们可以通过读取wParam变量的值来获得它是哪个按键。然后我把keys[ ]数组中的对应的按钮单元设置为TRUE。通过这种方法,今后我可以通过读取数组来查找哪一个按键被按下。这种方法允许多个按键被同时按下。

case WM_KEYDOWN:        // 是否有按键被按下
{
    keys[wParam] = TRUE;    // 如果有,把它设置为TRUE 
    return 0;         // 返回

}

如果一个按钮弹起,我们可以通过读取wParam变量的值来获知是哪个按键。然后我们把keys[ ]数组中对应的按键单元设置为FALSE。这种方法当我们读取这个按键单元时,就能知道它是被按下了还是弹起了。键盘上的每一个按键都可以用一个0-255的数字表示。例如,我点击了一个代表40的按键,keys[40]设置为TRUE。当我松开,它被设置为FALSE。这就是我们如何使用keys[ ]存储按键状态的原理。

case WM_KEYUP:        // 是否有按键弹起
{
    keys[wParam] = FALSE;       // 如果是,设置它为FALSE 
    return 0;       // 返回

}

无论何时,当我们重置窗口的尺寸时,uMsg 变量都会被设置为WM_SIZE。我们读取lParam 变量中的LOWORD 和 HIWORD 变量的值,然后计算出窗口的新的宽和高。然后把新的宽和高传递给ReSizeGLScene()函数。OpenGL场景会被设置为新的宽和高的尺寸。

    case WM_SIZE:                           // 重置OpenGL窗口尺寸 
    {
        ReSizeGLScene(LOWORD(lParam),HIWORD(lParam));       // LoWord=Width, HiWord=Height
        return 0;                       // 返回 
    }
}

任何我们不关心的信息都会传递给DefWindowProc 函数,然后Windows系统会处理它们。

    // 把所有没有处理的消息传递给DefWindowProc函数
    return DefWindowProc(hWnd,uMsg,wParam,lParam);
}

这里是我们Windows应用程序的入口。在这里我们调用窗口创建程序,处理窗口消息,监听用户交互。

int WINAPI WinMain( HINSTANCE   hInstance,              // 实例 
            HINSTANCE   hPrevInstance,              // 以前的实例 
            LPSTR       lpCmdLine,              // 命令行参数

            int     nCmdShow)               // 窗口显示状态
{

我们设置两个变量。msg用来检测是有还有需要处理的消息队列。done变量初始化为FALSE。说明我们的程序还没有停止运行。如果done变量一直为FALSE,我们的程序会持续运行。一旦done变量从FALSE变为TRUE,我们的程序将要退出。

MSG msg;                                // windows系统信息结构
BOOL    done=FALSE;                         // 退出循环的布尔变量

这部分代码是可选的。它会弹出一个消息框询问是否你想要程序运行在全屏模式下。如果用户点击NO按钮,fullscreen变量从TRUE(默认为TRUE)变为FALSE,然后程序会运行在窗口模式下。

// 询问用户想要哪种窗口模式
if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)
{
    fullscreen=FALSE;                       // 窗口模式
}

这里是我们如何创建一个OpenGL窗口。我们传递标题,宽,高,颜色深度,和TRUE(全屏)或FALSE(窗口模式)到CreateGLWindow函数。就是这样。我很高兴使用这些简便的代码。如果窗口因为某些原因没有创建成功,返回FALSE,然后程序退出(return 0)。

// 创建我们的OpenGL窗口
if (!CreateGLWindow("NeHe's OpenGL Framework",640,480,16,fullscreen))
{
    return 0;         // 如果窗口没有创建,退出程序
}

这里是我们循环的开始。如果done变量等于FALSE,重复循环。

while(!done)        // 重复循环,知道done等于TRUE
{

首先我们要检查是否存在任何的窗口消息队列。通过PeekMessage() 函数我们能在不暂停程序的情况下检查消息队列。很多程序使用GetMessage()函数。它很好用,但是使用GetMessage()函数在它接收到一个paint消息或其他的某些窗口消息前,你的程序不能做任何事情。

if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))      // 有没有消息队列
{

下面的这部分代码用来检查是否有退出消息。如果当期的消息是PostQuitMessage(0)产生的WM_QUIT消息,done变量将被设置为TRUE,程序退出。

 if (msg.message==WM_QUIT)     // 时候接收到退出消息
{
    done=TRUE;    // 如果是,done设置为TRUE

}
else           // 如果不是,处理窗口消息
{

如果消息不是退出消息,我们翻译消息,然后发送消息,让WndProc() 函数或者是Windows操作系统处理它。

        TranslateMessage(&msg);             // 翻译消息 
        DispatchMessage(&msg);              // 发送消息 
    }
}
else         // 如果没有任何消息
{

如果这里没有任何消息,我们将开始绘制我们的OpenGL场景。下面的第一行代码检查窗口是否处于活动状态。如果ESC按键被点击,done变量设置为TRUE,程序退出。

// 绘制场景。监听ESC按键和DrawGLScene() 函数发出的退出消息。
if (active)                     // 程序是否处于活动状态
{
    if (keys[VK_ESCAPE])                // 监听ESC按键 
    {
        done=TRUE;              // ESC标识退出 
    }
    else                        // 不退出,刷新屏幕 
    {

如果程序处于活动状态并且ESC没有点击,我们渲染场景,然后交换缓冲区(通过使用双缓冲区可以实现流程的动画)。通过使用双缓冲区,我们在一个看不到的隐藏屏幕上绘制所有的东西。当交换缓冲区时,显示的屏幕变为隐藏的屏幕,隐藏的屏幕变为显示的屏幕。这就是我们看不到场景绘制的原因。它只用来即时显示。

        DrawGLScene();              // 绘制场景 
        SwapBuffers(hDC);           // 交换缓冲区(双缓冲区) 
    }
}

下面的一小部分代码是最近新加上来的。它允许我们通过点击F1按键完成全屏模式和窗口模式的反复切换。

        if (keys[VK_F1])         // F1按键是否按下 
        {
            keys[VK_F1]=FALSE;       // 如果按下,设置为FALSE 
            KillGLWindow();         // 杀死当前窗口 
            fullscreen=!fullscreen;        // 切换全屏/窗口模式 
            // 重新创建我们的OpenGL窗口 
            if (!CreateGLWindow("NeHe's OpenGL Framework",640,480,16,fullscreen))
            {
                return 0;               // 如果窗口没有创建,退出 
            }
        }
    }
}

如果done变量不等于FALSE,程序退出。我们正常销毁OpenGL窗口,以便于所有的资源可以释放,然后退出程序。   

   // 关闭 
    KillGLWindow();                             // 销毁窗口 
    return (msg.wParam);                            // 退出程序
}

这个教程中,我已试着尽量详细解释一切细节,每一步都与设置有关,并创建了一个全屏OpenGL程序,点击ECS按键程序退出,并且可以监视窗口是否处于活动状态。我花了2个星期的时间编写代码,一周的时间修改bug和讨论编程指导,花了2天(整整22个小时)的时间编写HTML文件。如果你有什么建议或问题,请给我发E-mail。如果你觉得我哪里写的不正确,或者哪里可以改进,请通知我。我想要提供一个最好的OpenGL教程,我会很高兴收到你的反馈信息。

创建一个OpenGL窗口: 在这个教程里,我将教你在Windows环境中创建OpenGL程序.它将显示一个空的OpenGL窗口,可以在窗口和全屏模式下切换,按ESC退出.它是我们以后应用程序的框架. 理解OpenGL如何工作非常重要,你可以在教程的末尾下载源程序,但我强烈建议你至少读一遍教程,然后再开始编程. 2.你的第一个多边形: 在第一个教程的基础上,我们添加了一个三角形和一个四边形。也许你认为这很简单,但你已经迈出了一大步,要知道任何在OpenGL中绘制的模型都会被分解为这两种简单的图形。 读完了这一课,你会学到如何在空间放置模型,并且会知道深度缓存的概念。 3.添加颜色: 作为第二课的扩展,我将叫你如何使用颜色。你将理解两种着色模式,在左图中,三角形用的是光滑着色,四边形用的是平面着色。 注意三角形上的颜色是如何混合的。 颜色为OpenGlL 工程增加很多。通过理解平面着色(flat coloring)和平滑着色(smooth coloring),你能显著的改善你的OpenGL Demo的样子。 4.旋转: 在这一课里,我将教会你如何旋转三角形和四边形。左图中的三角形沿Y轴旋转,四边形沿着X 轴旋转。 这一章将引入两个变量, rtri 被用来存储三角形的角度, rquad存储四边形的角度。 和容易创建一个多边形组成的场景。让这些物体动起来是整个场景变得生动起来。在后面的课程钟我将教给你如何绕屏幕上的一个点旋转物体,使得物体绕屏幕而不是它的轴转动。 5.3D形体: 既然我们已经领会到多边形,方形,色彩和旋转。现在该建立3D物体了。我将使用多边形和矩形c创建3D物体。这次我们将扩展上一章的教程,并且将三角形转换成一个彩色的棱锥,把正方形变为一个实心正方体。棱锥使用混合色,正方体每个面使用一种颜色。在3D空间创建物体可能很费时间,但是所获得的结果(收获)值得这样做。充分发挥你的想象力吧。 6.纹理映射: 你想要它,它现在就在这里了,那就是 ... 纹理映射!!!在这一章我将教会你如何将一幅位图(bitmap)映射到正方体的六个面上去。我们将使用第一章的OpenGL代码来创建工程。创建一个空的窗口比修改上一课的代码更容易。 你将会发现第一章的代码在对于快速创建工程来说是及其有价值的。第一章的代码为你设置好了一切,你所需要做的只是集中精力为效果编程。 7.纹理滤波, 光照和键盘控制: 好的,我希望到现在你已经理解了所有的东西,因为这是一个巨大的教程。我想教给你两个新的方法来过滤(filter)你的纹理,简单的光照,键盘控制并且还可能更多 :) .如果你对到这一课为止你所学的东西并不充满信心,那就回头复习一下。玩一下其它课程的代码,不要操之过急。最好专心把每一课学好,而不是蜻蜓点水,只知道如何把东西做出来。 8.混合 有理由等一下,一个来自很酷的Hypercosm的程序员伙伴问(我)他是否可以写一章关于混合的教程。第八课通常正是讲混合的,所以太巧了。这一章教程扩展了第七章。混合是一项很酷的技术 .. 我希望你们能好好享受这一章教程。这一章的作者是Tom Stanis他在这制作一章上花费了很多精力,所以让他知道你觉得怎么样。混合可不是一个好讲的话题。 9.在3D空间中移动位图: 这一章覆盖了一些你们要求的主题,你想知道如何移动你在3D屏幕空间上创造的物体。你想要知道如何在屏幕上绘制一幅位图,并且位图的黑色部分不会覆盖它后面的东西。你想要简单的动画,想要更多的混合的应用,这一章将教会你所有这些。You'll notice there's no spinning boxes(yaker:很惭愧这一句我不是很明白)。前面的课程覆盖了OpenGL的基础,每一章都基于前面的内容。前面的课程涵盖了基础的OpenGL,每一课都是在前一课的基础上创建的。这一课是前面几课知识的综合,当你学习这课时,请确保你已经掌握了前面几课的知识。 10.加载3D世界,并在其中漫游: 你一直期待的教程来了!这一章友一个叫Lionel Brites的伙伴制作。这一课里你讲学到如何导入一个3D世界。代码仍然使用第一章的,但是,课程页面只是解释了新的部分,包括导入3D场景,在3D世界中移动。下载VC++代码并且在你阅读教程的同时阅读代码。按[B]键控制混合,[F]键控制滤波,[L]键控制光照(但光并不随场景移动),还有[Page UP]和[Page Down]键。我希望你能喜欢Lionel对网站的贡献。我有空的时候我会让这个教程更容易学习。 11.旗帜效果
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值