新书推介:《语义网技术体系》
作者:瞿裕忠,胡伟,程龚
   XML论坛     W3CHINA.ORG讨论区     计算机科学论坛     SOAChina论坛     Blog     开放翻译计划     新浪微博  
 
  • 首页
  • 登录
  • 注册
  • 软件下载
  • 资料下载
  • 核心成员
  • 帮助
  •   Add to Google

    >> 本版讨论高级C/C++编程、代码重构(Refactoring)、极限编程(XP)、泛型编程等话题
    [返回] 计算机科学论坛计算机技术与应用『 C/C++编程思想 』 → [推荐]NeHe OpenGL教程(中英文版附带VC++源码)Lesson 07-lesson 08 查看新帖用户列表

      发表一个新主题  发表一个新投票  回复主题  (订阅本版) 您是本帖的第 18813 个阅读者浏览上一篇主题  刷新本主题   树形显示贴子 浏览下一篇主题
     * 贴子主题: [推荐]NeHe OpenGL教程(中英文版附带VC++源码)Lesson 07-lesson 08 举报  打印  推荐  IE收藏夹 
       本主题类别:     
     一分之千 帅哥哟,离线,有人找我吗?射手座1984-11-30
      
      
      威望:1
      等级:研一(随老板参加了WWW大会还和Tim Berners-Lee合了影^_^)
      文章:632
      积分:4379
      门派:XML.ORG.CN
      注册:2006/12/31

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给一分之千发送一个短消息 把一分之千加入好友 查看一分之千的个人资料 搜索一分之千在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看一分之千的博客楼主
    发贴心情 [推荐]NeHe OpenGL教程(中英文版附带VC++源码)Lesson 07-lesson 08

    第七课第八课源码下载

    第七课 中文

    按此在新窗口浏览图片光照和键盘控制:

    在这一课里,我们将添加光照和键盘控制,它让程序看起来更美观。

      
       
       
    这一课我会教您如何使用三种不同的纹理滤波方式。教您如何使用键盘来移动场景中的对象,还会教您在OpenGL场景中应用简单的光照。这一课包含了很多内容,如果您对前面的课程有疑问的话,先回头复习一下。进入后面的代码之前,很好的理解基础知识十分重要。
    我们还是在第一课的代码上加以修改。跟以前不一样的是,只要有任何大的改动,我都会写出整段代码。程序开始,我们先加上几个新的变量。  
       
       
    下面几行是新的。我们增加三个布尔变量。 light 变量跟踪光照是否打开。变量lp和fp用来存储'L' 和'F'键是否按下的状态。后面我会解释这些变量的重要性。现在,先放在一边吧。  
       

    BOOL light;         // 光源的开/关
    BOOL lp;         // L键按下了么?
    BOOL fp;         // F键按下了么?

       
    现在设置5个变量来控制绕x轴和y轴旋转角度的步长,以及绕x轴和y轴的旋转速度。另外还创建了一个z变量来控制进入屏幕深处的距离。  
       

    GLfloat xrot;         // X 旋转
    GLfloat yrot;         // Y 旋转
    GLfloat xspeed;         // X 旋转速度
    GLfloat yspeed;         // Y 旋转速度

    GLfloat z=-5.0f;        // 深入屏幕的距离

       
    接着设置用来创建光源的数组。我们将使用两种不同的光。第一种称为环境光。环境光来自于四面八方。所有场景中的对象都处于环境光的照射中。第二种类型的光源叫做漫射光。漫射光由特定的光源产生,并在您的场景中的对象表面上产生反射。处于漫射光直接照射下的任何对象表面都变得很亮,而几乎未被照射到的区域就显得要暗一些。这样在我们所创建的木板箱的棱边上就会产生的很不错的阴影效果。
    创建光源的过程和颜色的创建完全一致。前三个参数分别是RGB三色分量,最后一个是alpha通道参数。
    因此,下面的代码我们得到的是半亮(0.5f)的白色环境光。如果没有环境光,未被漫射光照到的地方会变得十分黑暗。  
       

    GLfloat LightAmbient[]= { 0.5f, 0.5f, 0.5f, 1.0f };     // 环境光参数

       
    下一行代码我们生成最亮的漫射光。所有的参数值都取成最大值1.0f。它将照在我们木板箱的前面,看起来挺好。  
       

    GLfloat LightDiffuse[]= { 1.0f, 1.0f, 1.0f, 1.0f };     // 漫射光参数

       
    最后我们保存光源的位置。前三个参数和glTranslate中的一样。依次分别是XYZ轴上的位移。由于我们想要光线直接照射在木箱的正面,所以XY轴上的位移都是0.0f。第三个值是Z轴上的位移。为了保证光线总在木箱的前面,所以我们将光源的位置朝着观察者(就是您哪。)挪出屏幕。我们通常将屏幕也就是显示器的屏幕玻璃所处的位置称作Z轴的0.0f点。所以Z轴上的位移最后定为2.0f。假如您能够看见光源的话,它就浮在您显示器的前方。当然,如果木箱不在显示器的屏幕玻璃后面的话,您也无法看见箱子。『译者注:我很欣赏NeHe的耐心。说真的有时我都打烦了,这么简单的事他这么废话干嘛?但如果什么都清楚,您还会翻着这样的页面看个没完么?』
    最后一个参数取为1.0f。这将告诉OpenGL这里指定的坐标就是光源的位置,以后的教程中我会多加解释。  
       

    GLfloat LightPosition[]= { 0.0f, 0.0f, 2.0f, 1.0f };     // 光源位置

       
    filter 变量跟踪显示时所采用的纹理类型。第一种纹理(texture 0) 使用gl_nearest(不光滑)滤波方式构建。第二种纹理 (texture 1) 使用gl_linear(线性滤波) 方式,离屏幕越近的图像看起来就越光滑。第三种纹理 (texture 2) 使用 mipmapped滤波方式,这将创建一个外观十分优秀的纹理。根据我们的使用类型,filter 变量的值分别等于 0, 1 或 2 。下面我们从第一种纹理开始。
    GLuint texture[3] 为三种不同纹理分配储存空间。它们分别位于在 texture[0], texture[1] 和 texture[2]中。  
       

    GLuint filter;         // 滤波类型
    GLuint texture[3];        // 3种纹理的储存空间

       
    现在载入一个位图,并用它创建三种不同的纹理。这一课使用glaux辅助库来载入位图,因此在编译时您应该确认是否包含了glaux库。我知道Delphi和VC++都包含了glaux库,但别的语言不能保证都有。『译者注:glaux是OpenGL辅助库,根据OpenGL的跨平台特性,所有平台上的代码都应通用。但辅助库不是正式的OpenGL标准库,没有出现在所有的平台上。但正好在Win32平台上可用。呵呵,BCB当然也没问题了。』这里我只对新增的代码做注解。如果您对某行代码有疑问的话,请查看教程六。那一课很详细的解释了载入、创建纹理的内容。
    在上一段代码后面及ReSizeGLScene()之前的位置,我们增加了下面的代码。这和第六课中载入位图的代码几乎相同。  
       
       
    这段代码调用前面的代码载入位图,并将其转换成3个纹理。Status 变量跟踪纹理是否已载入并被创建了。  
       

    int LoadGLTextures()        // 载入位图并转换成纹理
    {
     int Status=FALSE;       // 状态指示器

     AUX_RGBImageRec *TextureImage[1];     // 创建纹理的存储空间

     memset(TextureImage,0,sizeof(void *)*1);    // 将指针设为 NULL

       
    现在载入位图并转换成纹理。TextureImage[0]=LoadBMP("Data/Crate.bmp")调用我们的LoadBMP()函数。Data目录下的Crate.bmp将被载入。如果一切正常,图像数据将存放在TextureImage[0]。Status变量被设为TRUE,我们将开始创建纹理。  
       

     // 载入位图,检查有错,或位图不存在的话退出
     if (TextureImage[0]=LoadBMP("Data/Crate.bmp"))
     {
      Status=TRUE;       // 状态设为 TRUE

       
    现在我们已经将图像数据载入TextureImage[0]。我们将用它来创建3个纹理。下面的行告诉OpenGL我们要创建三个纹理,它们将存放在texture[0], texture[1] 和 texture[2] 中。  
       

      glGenTextures(3, &texture[0]);     // 创建纹理

       
    第六课中我们使用了线性滤波的纹理贴图。这需要机器有相当高的处理能力,但它们看起来很不错。这一课中,我们接着要创建的第一种纹理使用 GL_NEAREST方式。从原理上讲,这种方式没有真正进行滤波。它只占用很小的处理能力,看起来也很差。唯一的好处是这样我们的工程在很快和很慢的机器上都可以正常运行。
    您会注意到我们在 MIN 和 MAG 时都采用了GL_NEAREST,你可以混合使用 GL_NEAREST 和 GL_LINEAR。纹理看起来效果会好些,但我们更关心速度,所以全采用低质量贴图。MIN_FILTER在图像绘制时小于贴图的原始尺寸时采用。MAG_FILTER在图像绘制时大于贴图的原始尺寸时采用。  
       

      // 创建 Nearest 滤波贴图
      glBindTexture(GL_TEXTURE_2D, texture[0]);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
      glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);

       
    The next texture we build is the same type of texture we used in tutorial six. Linear filtered. The only thing that has changed is that we are storing this texture in texture[1] instead of texture[0] because it's our second texture. If we stored it in texture[0] like above, it would overwrite the GL_NEAREST texture (the first texture).   
       

      // 创建线性滤波纹理
      glBindTexture(GL_TEXTURE_2D, texture[1]);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
      glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);

       
    下面是创建纹理的新方法。 Mipmapping!『译者注:这个词的中文我翻不出来,不过没关系。看完这一段,您就知道意思最重要。』您可能会注意到当图像在屏幕上变得很小的时候,很多细节将会丢失。刚才还很不错的图案变得很难看。当您告诉OpenGL创建一个 mipmapped的纹理后,OpenGL将尝试创建不同尺寸的高质量纹理。当您向屏幕绘制一个 mipmapped纹理的时候,OpenGL将选择它已经创建的外观最佳的纹理(带有更多细节)来绘制,而不仅仅是缩放原先的图像(这将导致细节丢失)。
    我曾经说过有办法可以绕过OpenGL对纹理宽度和高度所加的限制——64、128、256,等等。办法就是 gluBuild2DMipmaps。据我的发现,您可以使用任意的位图来创建纹理。OpenGL将自动将它缩放到正常的大小。
    因为是第三个纹理,我们将它存到texture[2]。这样本课中的三个纹理全都创建好了。  
       

      // 创建 MipMapped 纹理
      glBindTexture(GL_TEXTURE_2D, texture[2]);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_NEAREST);
       
    下面一行生成 mipmapped 纹理。我们使用三种颜色(红,绿,蓝)来生成一个2D纹理。TextureImage[0]->sizeX 是位图宽度,extureImage[0]->sizeY 是位图高度,GL_RGB意味着我们依次使用RGB色彩。GL_UNSIGNED_BYTE 意味着纹理数据的单位是字节。TextureImage[0]->data指向我们创建纹理所用的位图。   
       

      gluBuild2DMipmaps(GL_TEXTURE_2D, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
     }

       
    现在释放用来存放位图数据的内存。我们先查看位图数据是否存放在 TextureImage[0] 中,如果有,删掉。然后释放位图结构以确保内存被释放。  
       

     if (TextureImage[0])       // 纹理是否存在
     {
      if (TextureImage[0]->data)     // 纹理图像是否存在
      {
       free(TextureImage[0]->data);    // 释放纹理图像占用的内存
      }

      free(TextureImage[0]);      // 释放图像结构
     }

       
    最后我们返回 status 变量。如果一切OK,status 变量的值为TRUE。否则为FALSE。  
       

     return Status;        // 返回 Status 变量
    }

       
    接着应该载入纹理并初始化OpenGL设置了。InitGL函数的第一行使用上面的代码载入纹理。创建纹理之后,我们调用
    glEnable(GL_TEXTURE_2D)启用2D纹理映射。阴影模式设为平滑阴影( smooth shading )。背景色设为黑色,我们启用深度测试,然后我们启用优化透视计算。  
       
       
    现在开始设置光源。下面下面一行设置环境光的发光量,光源light1开始发光。这一课的开始处我们我们将环境光的发光量存放在LightAmbient数组中。现在我们就使用此数组(半亮度环境光)。在int InitGL(GLvoid)函数中添加下面的代码。  
       

     glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient);    // 设置环境光

       
    接下来我们设置漫射光的发光量。它存放在LightDiffuse数组中(全亮度白光)。  
       

     glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse);    // 设置漫射光

       
    然后设置光源的位置。位置存放在 LightPosition 数组中(正好位于木箱前面的中心,X-0.0f,Y-0.0f,Z方向移向观察者2个单位<位于屏幕外面>)。  
       

     glLightfv(GL_LIGHT1, GL_POSITION,LightPosition);   // 设置光源位置

       
    最后,我们启用一号光源。我们还没有启用GL_LIGHTING,所以您看不见任何光线。记住:只对光源进行设置、定位、甚至启用,光源都不会工作。除非我们启用GL_LIGHTING。  
       

     glEnable(GL_LIGHT1);       // 启用一号光源

       
    下一段代码绘制贴图立方体。我只对新增的代码进行注解。如果您对没有注解的代码有疑问,回头看看第六课。  
       

    int DrawGLScene(GLvoid)        // 从这里开始进行所有的绘制
    {
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);   // 清除屏幕和深度缓存
     glLoadIdentity();       // 重置当前的模型观察矩阵

       
    下三行代码放置并旋转贴图立方体。glTranslatef(0.0f,0.0f,z)将立方体沿着Z轴移动Z单位。glRotatef(xrot,1.0f,0.0f,0.0f)将立方体绕X轴旋转xrot。glRotatef(yrot,0.0f,1.0f,0.0f)将立方体绕Y轴旋转yrot。  
       

     glTranslatef(0.0f,0.0f,z);      // 移入/移出屏幕 z 个单位

     glRotatef(xrot,1.0f,0.0f,0.0f);      // 绕X轴旋转
     glRotatef(yrot,0.0f,1.0f,0.0f);      // 绕Y轴旋转

       
    下一行与我们在第六课中的类似。有所不同的是,这次我们绑定的纹理是texture[filter],而不是上一课中的texture[0]。任何时候,我们按下F键,filter 的值就会增加。如果这个数值大于2,变量filter 将被重置为0。程序初始时,变量filter 的值也将设为0。使用变量filter 我们就可以选择三种纹理中的任意一种。  
       

     glBindTexture(GL_TEXTURE_2D, texture[filter]);    // 选择由filter决定的纹理

     glBegin(GL_QUADS);       // 开始绘制四边形

       
    glNormal3f是这一课的新东西。Normal就是法线的意思,所谓法线是指经过面(多边形)上的一点且垂直于这个面(多边形)的直线。使用光源的时候必须指定一条法线。法线告诉OpenGL这个多边形的朝向,并指明多边形的正面和背面。如果没有指定法线,什么怪事情都可能发生:不该照亮的面被照亮了,多边形的背面也被照亮....。对了,法线应该指向多边形的外侧。

    看着木箱的前面您会注意到法线与Z轴正向同向。这意味着法线正指向观察者-您自己。这正是我们所希望的。对于木箱的背面,也正如我们所要的,法线背对着观察者。如果立方体沿着X或Y轴转个180度的话,前侧面的法线仍然朝着观察者,背面的法线也还是背对着观察者。换句话说,不管是哪个面,只要它朝着观察者这个面的法线就指向观察者。由于光源紧邻观察者,任何时候法线对着观察者时,这个面就会被照亮。并且法线越朝着光源,就显得越亮一些。如果您把观察点放到立方体内部,你就会法线里面一片漆黑。因为法线是向外指的。如果立方体内部没有光源的话,当然是一片漆黑。

      
       

      // 前侧面
      glNormal3f( 0.0f, 0.0f, 1.0f);     // 法线指向观察者
      glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f,  1.0f); 
      glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f,  1.0f); 
      glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f,  1.0f,  1.0f); 
      glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f,  1.0f,  1.0f); 
      // 后侧面
      glNormal3f( 0.0f, 0.0f,-1.0f);     // 法线背向观察者
      glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f); 
      glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f,  1.0f, -1.0f); 
      glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f,  1.0f, -1.0f); 
      glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f); 
      // 顶面
      glNormal3f( 0.0f, 1.0f, 0.0f);     // 法线向上
      glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f,  1.0f, -1.0f); 
      glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f,  1.0f,  1.0f); 
      glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f,  1.0f,  1.0f); 
      glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f,  1.0f, -1.0f); 
      // 底面
      glNormal3f( 0.0f,-1.0f, 0.0f);     // 法线朝下
      glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f); 
      glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f); 
      glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f,  1.0f); 
      glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f,  1.0f); 
      // 右侧面
      glNormal3f( 1.0f, 0.0f, 0.0f);     // 法线朝右
      glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f); 
      glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f,  1.0f, -1.0f); 
      glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f,  1.0f,  1.0f); 
      glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f,  1.0f); 
      // 左侧面
      glNormal3f(-1.0f, 0.0f, 0.0f);     // 法线朝左
      glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f); 
      glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f,  1.0f); 
      glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f,  1.0f,  1.0f); 
      glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f,  1.0f, -1.0f); 
     glEnd();        // 四边形绘制结束

       
    下两行代码将xot和yrot的旋转值分别增加xspeed和yspeed个单位。xspeed和yspeed的值越大,立方体转得就越快。  
       

     xrot+=xspeed;        // xrot 增加 xspeed 单位
     yrot+=yspeed;        // yrot 增加 yspeed 单位
     return TRUE; 
    }

       
    现在转入WinMain()主函数。我们将在这里增加开关光源、旋转木箱、切换过滤方式以及将木箱移近移远的控制代码。在接近WinMain()函数结束的地方你会看到SwapBuffers(hDC)这行代码。然后就在这一行后面添加如下的代码。
    代码将检查L键是否按下过。如果L键已按下,但lp的值不是false的话,意味着L键还没有松开,这时什么都不会发生。  
       

        SwapBuffers(hDC);    // 交换缓存
        if (keys['L'] && !lp)    // L 键已按下并且松开了?
        {

       
    如果lp的值是false的话,意味着L键还没按下,或者已经松开了,接着lp将被设为TRUE。同时检查这两个条件的原因是为了防止L键被按住后,这段代码被反复执行,并导致窗体不停闪烁。
    lp设为true之后,计算机就知道L键按过了,我们则据此可以切换光源的开/关:布尔变量light控制光源的开关。  
       

         lp=TRUE;    // lp 设为 TRUE
         light=!light;    // 切换光源的 TRUE/FALSE

       
    Now we check to see what light ended up being. The first line translated to english means: If light equals false. So if you put it all together, the lines do the following: If light equals false, disable lighting. This turns all lighting off. The command 'else' translates to: if it wasn't false. So if light wasn't false, it must have been true, so we turn lighting on.   
       

         if (!light)    // 如果没有光源
         {
          glDisable(GL_LIGHTING);  // 禁用光源
         }
         else     // 否则
         {
          glEnable(GL_LIGHTING);  // 启用光源
         }
        }

       
    下面的代码查看是否松开了"L"键。如果松开,变量lp将设为false。这意味着"L"键没有按下。如果不作此检查,光源第一次打开之后,就无法再关掉了。计算机会以为"L"键一直按着呢。  
       

        if (!keys['L'])     // L键松开了么?
        {
         lp=FALSE;    // 若是,则将lp设为FALSE
        }

       
    然后对"F"键作相似的检查。如果有按下"F"键并且"F"键没有处于按着的状态或者它就从没有按下过,将变量fp设为true。这意味着这个键正被按着呢。接着将filter变量加一。如果filter变量大于2(因为这里我们的使用的数组是texture[3],大于2的纹理不存在),我们重置filter变量为0。  
       

        if (keys['F'] && !fp)    // F键按下了么?
        {
         fp=TRUE;    // fp 设为 TRUE
         filter+=1;    // filter的值加一
         if (filter>2)    // 大于2了么?
         {
          filter=0;   // 若是重置为0
         }
        }
        if (!keys['F'])     // F键放开了么?
        {
         fp=FALSE;    // 若是fp设为FALSE
        }

       
    这四行检查是否按下了PageUp键。若是的话,减少z变量的值。这样DrawGLScene函数中包含的glTranslatef(0.0f,0.0f,z)调用将使木箱离观察者更远一点。  
       

        if (keys[VK_PRIOR])    // PageUp按下了?
        {
         z-=0.02f;    // 若按下,将木箱移向屏幕内部
        }

       
    接着四行检查PageDown键是否按下,若是的话,增加z变量的值。这样DrawGLScene函数中包含的glTranslatef(0.0f,0.0f,z)调用将使木箱向着观察者移近一点。  
       

        if (keys[VK_NEXT])    // PageDown按下了么
        {
         z+=0.02f;    // 若按下的话,将木箱移向观察者
        }

       
    现在检查方向键。按下左右方向键xspeed相应减少或增加。按下上下方向键yspeed相应减少或增加。记住在以后的教程中如果xspeed、yspeed的值增加的话,立方体就转的更快。如果一直按着某个方向键,立方体会在那个方向上转的越快。  
       

        if (keys[VK_UP])    // Up方向键按下了么?
        {
         xspeed-=0.01f;    // 若是,减少xspeed
        }
        if (keys[VK_DOWN])    // Down方向键按下了么?
        {
         xspeed+=0.01f;    // 若是,增加xspeed
        }
        if (keys[VK_RIGHT])    // Right方向键按下了么?
        {
         yspeed+=0.01f;    // 若是,增加yspeed
        }
        if (keys[VK_LEFT])    // Left方向键按下了么?
        {
         yspeed-=0.01f;    // 若是, 减少yspeed
        }

       
    像前几课一样,我们最后还需要更正窗体的标题。  
       

        if (keys[VK_F1])    // F1按下了么?
        {
         keys[VK_F1]=FALSE;   // 若是将其设为FALSE
         KillGLWindow();    // 销毁当前窗口
         fullscreen=!fullscreen;   // 切换全屏/窗口模式
         // 重建GL窗口
         if (!CreateGLWindow("NeHe's Textures, Lighting & Keyboard Tutorial",640,480,16,fullscreen))
         {
          return 0;   // 若无法创建窗口,程序退出
         }
        }
       }
      }
     }

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

       
    这一课完了之后,您应该学会创建和使用这三种不同的纹理映射过滤方式。并使用键盘和场景中的对象交互。最后,您应该学会在场景中应用简单的光源,使得场景看起来更逼真。


       收藏   分享  
    顶(2)
      




    ----------------------------------------------
    越学越无知

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/10/13 10:06:00
     
     一分之千 帅哥哟,离线,有人找我吗?射手座1984-11-30
      
      
      威望:1
      等级:研一(随老板参加了WWW大会还和Tim Berners-Lee合了影^_^)
      文章:632
      积分:4379
      门派:XML.ORG.CN
      注册:2006/12/31

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给一分之千发送一个短消息 把一分之千加入好友 查看一分之千的个人资料 搜索一分之千在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看一分之千的博客2
    发贴心情 
    Lesson 07
       
    In this tutorial I'll teach you how to use three different texture filters. I'll teach you how to move an object using keys on the keyboard, and I'll also teach you how to apply simple lighting to your OpenGL scene. Lots covered in this tutorial, so if the previous tutorials are giving you problems, go back and review. It's important to have a good understanding of the basics before you jump into the following code.

    We're going to be modifying the code from lesson one again. As usual, if there are any major changes, I will write out the entire section of code that has been modified. We'll start off by adding a few new variables to the program.   
       

    #include <windows.h>       // Header File For Windows
    #include <stdio.h>       // Header File For Standard Input/Output ( ADD )
    #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

    HDC  hDC=NULL;       // Private GDI Device Context
    HGLRC  hRC=NULL;       // Permanent Rendering Context
    HWND  hWnd=NULL;       // Holds Our Window Handle
    HINSTANCE hInstance;       // Holds The Instance Of The Application

    bool  keys[256];       // Array Used For The Keyboard Routine
    bool  active=TRUE;       // Window Active Flag
    bool  fullscreen=TRUE;      // Fullscreen Flag

       
    The lines below are new. We're going to add three boolean variables. BOOL means the variable can only be TRUE or FALSE. We create a variable called light to keep track of whether or not the lighting is on or off. The variables lp and fp are used to store whether or not the 'L' or 'F' key has been pressed. I'll explain why we need these variables later on in the code. For now, just know that they are important.   
       

    BOOL light;         // Lighting ON / OFF
    BOOL lp;         // L Pressed?
    BOOL fp;         // F Pressed?

       
    Now we're going to set up five variables that will control the angle on the x axis (xrot), the angle on the y axis (yrot), the speed the crate is spinning at on the x axis (xspeed), and the speed the crate is spinning at on the y axis (yspeed). We'll also create a variable called z that will control how deep into the screen (on the z axis) the crate is.   
       

    GLfloat xrot;         // X Rotation
    GLfloat yrot;         // Y Rotation
    GLfloat xspeed;         // X Rotation Speed
    GLfloat yspeed;         // Y Rotation Speed

    GLfloat z=-5.0f;        // Depth Into The Screen

       
    Now we set up the arrays that will be used to create the lighting. We'll use two different types of light. The first type of light is called ambient light. Ambient light is light that doesn't come from any particular direction. All the objects in your scene will be lit up by the ambient light. The second type of light is called diffuse light. Diffuse light is created by your light source and is reflected off the surface of an object in your scene. Any surface of an object that the light hits directly will be very bright, and areas the light barely gets to will be darker. This creates a nice shading effect on the sides of our crate.

    Light is created the same way color is created. If the first number is 1.0f, and the next two are 0.0f, we will end up with a bright red light. If the third number is 1.0f, and the first two are 0.0f, we will have a bright blue light. The last number is an alpha value. We'll leave it at 1.0f for now.

    So in the line below, we are storing the values for a white ambient light at half intensity (0.5f). Because all the numbers are 0.5f, we will end up with a light that's halfway between off (black) and full brightness (white). Red, blue and green mixed at the same value will create a shade from black(0.0f) to white(1.0f). Without an ambient light, spots where there is no diffuse light will appear very dark.   
       

    GLfloat LightAmbient[]= { 0.5f, 0.5f, 0.5f, 1.0f };     // Ambient Light Values ( NEW )

       
    In the next line we're storing the values for a super bright, full intensity diffuse light. All the values are 1.0f. This means the light is as bright as we can get it. A diffuse light this bright lights up the front of the crate nicely.   
       

    GLfloat LightDiffuse[]= { 1.0f, 1.0f, 1.0f, 1.0f };     // Diffuse Light Values ( NEW )

       
    Finally we store the position of the light. The first three numbers are the same as glTranslate's three numbers. The first number is for moving left and right on the x plane, the second number is for moving up and down on the y plane, and the third number is for moving into and out of the screen on the z plane. Because we want our light hitting directly on the front of the crate, we don't move left or right so the first value is 0.0f (no movement on x), we don't want to move up and down, so the second value is 0.0f as well. For the third value we want to make sure the light is always in front of the crate. So we'll position the light off the screen, towards the viewer. Lets say the glass on your monitor is at 0.0f on the z plane. We'll position the light at 2.0f on the z plane. If you could actually see the light, it would be floating in front of the glass on your monitor. By doing this, the only way the light would be behind the crate is if the crate was also in front of the glass on your monitor. Of course if the crate was no longer behind the glass on your monitor, you would no longer see the crate, so it doesn't matter where the light is. Does that make sense?

    There's no real easy way to explain the third parameter. You should know that -2.0f is going to be closer to you than -5.0f. and -100.0f would be WAY into the screen. Once you get to 0.0f, the image is so big, it fills the entire monitor. Once you start going into positive values, the image no longer appears on the screen cause it has "gone past the screen". That's what I mean when I say out of the screen. The object is still there, you just can't see it anymore.

    Leave the last number at 1.0f. This tells OpenGL the designated coordinates are the position of the light source. More about this in a later tutorial.   
       

    GLfloat LightPosition[]= { 0.0f, 0.0f, 2.0f, 1.0f };     // Light Position ( NEW )

       
    The filter variable below is to keep track of which texture to display. The first texture (texture 0) is made using gl_nearest (no smoothing). The second texture (texture 1) uses gl_linear filtering which smooths the image out quite a bit. The third texture (texture 2) uses mipmapped textures, creating a very nice looking texture. The variable filter will equal 0, 1 or 2 depending on the texture we want to use. We start off with the first texture.

    GLuint texture[3] creates storage space for the three different textures. The textures will be stored at texture[0], texture[1] and texture[2].   
       

    GLuint filter;         // Which Filter To Use
    GLuint texture[3];        // Storage for 3 textures

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

       
    Now we load in a bitmap, and create three different textures from it. This tutorial uses the glaux library to load in the bitmap, so make sure you have the glaux library included before you try compiling the code. I know Delphi, and Visual C++ both have glaux libraries. I'm not sure about other languages. I'm only going to explain what the new lines of code do, if you see a line I haven't commented on, and you're wondering what it does, check tutorial six. It explains loading, and building texture maps from bitmap images in great detail.

    Immediately after the above code, and before ReSizeGLScene(), we want to add the following section of code. This is the same code we used in lesson 6 to load in a bitmap file. Nothing has changed. If you're not sure what any of the following lines do, read tutorial six. It explains the code below in detail.   
       

    AUX_RGBImageRec *LoadBMP(char *Filename)     // Loads A Bitmap Image
    {
     FILE *File=NULL;       // File Handle

     if (!Filename)        // Make Sure A Filename Was Given
     {
      return NULL;       // If Not Return NULL
     }

     File=fopen(Filename,"r");      // Check To See If The File Exists

     if (File)        // Does The File Exist?
     {
      fclose(File);       // Close The Handle
      return auxDIBImageLoad(Filename);    // Load The Bitmap And Return A Pointer
     }
     return NULL;        // If Load Failed Return NULL
    }

       
    This is the section of code that loads the bitmap (calling the code above) and converts it into 3 textures. Status is used to keep track of whether or not the texture was loaded and created.   
       

    int LoadGLTextures()        // Load Bitmaps And Convert To Textures
    {
     int Status=FALSE;       // Status Indicator

     AUX_RGBImageRec *TextureImage[1];     // Create Storage Space For The Texture

     memset(TextureImage,0,sizeof(void *)*1);    // Set The Pointer To NULL

       
    Now we load the bitmap and convert it to a texture. TextureImage[0]=LoadBMP("Data/Crate.bmp") will jump to our LoadBMP() code. The file named Crate.bmp in the Data directory will be loaded. If everything goes well, the image data is stored in TextureImage[0], Status is set to TRUE, and we start to build our texture.   
       

     // Load The Bitmap, Check For Errors, If Bitmap's Not Found Quit
     if (TextureImage[0]=LoadBMP("Data/Crate.bmp"))
     {
      Status=TRUE;       // Set The Status To TRUE

       
    Now that we've loaded the image data into TextureImage[0], we'll use the data to build 3 textures. The line below tells OpenGL we want to build three textures, and we want the texture to be stored in texture[0], texture[1] and texture[2].   
       

      glGenTextures(3, &texture[0]);     // Create Three Textures

       
    In tutorial six, we used linear filtered texture maps. They require a hefty amount of processing power, but they look real nice. The first type of texture we're going to create in this tutorial uses GL_NEAREST. Basically this type of texture has no filtering at all. It takes very little processing power, and it looks real bad. If you've ever played a game where the textures look all blocky, it's probably using this type of texture. The only benefit of this type of texture is that projects made using this type of texture will usually run pretty good on slow computers.

    You'll notice we're using GL_NEAREST for both the MIN and MAG. You can mix GL_NEAREST with GL_LINEAR, and the texture will look a bit better, but we're intested in speed, so we'll use low quality for both. The MIN_FILTER is the filter used when an image is drawn smaller than the original texture size. The MAG_FILTER is used when the image is bigger than the original texture size.   
       

      // Create Nearest Filtered Texture
      glBindTexture(GL_TEXTURE_2D, texture[0]);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST); ( NEW )
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST); ( NEW )
      glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);

       
    The next texture we build is the same type of texture we used in tutorial six. Linear filtered. The only thing that has changed is that we are storing this texture in texture[1] instead of texture[0] because it's our second texture. If we stored it in texture[0] like above, it would overwrite the GL_NEAREST texture (the first texture).   
       

      // Create Linear Filtered Texture
      glBindTexture(GL_TEXTURE_2D, texture[1]);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
      glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);

       
    Now for a new way to make textures. Mipmapping! You may have noticed that when you make an image very tiny on the screen, alot of the fine details disappear. Patterns that used to look nice start looking real bad. When you tell OpenGL to build a mipmapped texture OpenGL tries to build different sized high quality textures. When you draw a mipmapped texture to the screen OpenGL will select the BEST looking texture from the ones it built (texture with the most detail) and draw it to the screen instead of resizing the original image (which causes detail loss).

    I had said in tutorial six there was a way around the 64,128,256,etc limit that OpenGL puts on texture width and height. gluBuild2DMipmaps is it. From what I've found, you can use any bitmap image you want (any width and height) when building mipmapped textures. OpenGL will automatically size it to the proper width and height.

    Because this is texture number three, we're going to store this texture in texture[2]. So now we have texture[0] which has no filtering, texture[1] which uses linear filtering, and texture[2] which uses mipmapped textures. We're done building the textures for this tutorial.   
       

      // Create MipMapped Texture
      glBindTexture(GL_TEXTURE_2D, texture[2]);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_NEAREST); ( NEW )

       
    The following line builds the mipmapped texture. We're creating a 2D texture using three colors (red, green, blue). TextureImage[0]->sizeX is the bitmaps width, TextureImage[0]->sizeY is the bitmaps height, GL_RGB means we're using Red, Green, Blue colors in that order. GL_UNSIGNED_BYTE means the data that makes the texture is made up of bytes, and TextureImage[0]->data points to the bitmap data that we're building the texture from.   
       

      gluBuild2DMipmaps(GL_TEXTURE_2D, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data); ( NEW )
     }

       
    Now we free up any ram that we may have used to store the bitmap data. We check to see if the bitmap data was stored in TextureImage[0]. If it was we check to see if the data has been stored. If data was stored, we erase it. Then we free the image structure making sure any used memory is freed up.   
       

     if (TextureImage[0])       // If Texture Exists
     {
      if (TextureImage[0]->data)     // If Texture Image Exists
      {
       free(TextureImage[0]->data);    // Free The Texture Image Memory
      }

      free(TextureImage[0]);      // Free The Image Structure
     }

       
    Finally we return the status. If everything went OK, the variable Status will be TRUE. If anything went wrong, Status will be FALSE.   
       

     return Status;        // Return The Status
    }

       
    Now we load the textures, and initialize the OpenGL settings. The first line of InitGL loads the textures using the code above. After the textures have been created, we enable 2D texture mapping with glEnable(GL_TEXTURE_2D). The shade mode is set to smooth shading, The background color is set to black, we enable depth testing, then we enable nice perspective calculations.   
       

    int InitGL(GLvoid)        // All Setup For OpenGL Goes Here
    {
     if (!LoadGLTextures())       // Jump To Texture Loading Routine
     {
      return FALSE;       // If Texture Didn't Load Return FALSE
     }

     glEnable(GL_TEXTURE_2D);      // Enable Texture Mapping
     glShadeModel(GL_SMOOTH);      // Enable Smooth Shading
     glClearColor(0.0f, 0.0f, 0.0f, 0.5f);     // Black Background
     glClearDepth(1.0f);       // Depth Buffer Setup
     glEnable(GL_DEPTH_TEST);      // Enables Depth Testing
     glDepthFunc(GL_LEQUAL);       // The Type Of Depth Testing To Do
     glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);   // Really Nice Perspective Calculations

       
    Now we set up the lighting. The line below will set the amount of ambient light that light1 will give off. At the beginning of this tutorial we stored the amount of ambient light in LightAmbient. The values we stored in the array will be used (half intensity ambient light).   
       

     glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient);    // Setup The Ambient Light

       
    Next we set up the amount of diffuse light that light number one will give off. We stored the amount of diffuse light in LightDiffuse. The values we stored in this array will be used (full intensity white light).   
       

     glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse);    // Setup The Diffuse Light

       
    Now we set the position of the light. We stored the position in LightPosition. The values we stored in this array will be used (right in the center of the front face, 0.0f on x, 0.0f on y, and 2 unit towards the viewer {coming out of the screen} on the z plane).   
       

     glLightfv(GL_LIGHT1, GL_POSITION,LightPosition);   // Position The Light

       
    Finally, we enable light number one. We haven't enabled GL_LIGHTING though, so you wont see any lighting just yet. The light is set up, and positioned, it's even enabled, but until we enable GL_LIGHTING, the light will not work.   
       

     glEnable(GL_LIGHT1);       // Enable Light One
     return TRUE;        // Initialization Went OK
    }

       
    In the next section of code, we're going to draw the texture mapped cube. I will comment a few of the line only because they are new. If you're not sure what the uncommented lines do, check tutorial number six.   
       

    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 View

       
    The next three lines of code position and rotate the texture mapped cube. glTranslatef(0.0f,0.0f,z) moves the cube to the value of z on the z plane (away from and towards the viewer). glRotatef(xrot,1.0f,0.0f,0.0f) uses the variable xrot to rotate the cube on the x axis. glRotatef(yrot,0.0f,1.0f,0.0f) uses the variable yrot to rotate the cube on the y axis.   
       

     glTranslatef(0.0f,0.0f,z);      // Translate Into/Out Of The Screen By z

     glRotatef(xrot,1.0f,0.0f,0.0f);      // Rotate On The X Axis By xrot
     glRotatef(yrot,0.0f,1.0f,0.0f);      // Rotate On The Y Axis By yrot

       
    The next line is similar to the line we used in tutorial six, but instead of binding texture[0], we are binding texture[filter]. Any time we press the 'F' key, the value in filter will increase. If this value is higher than two, the variable filter is set back to zero. When the program starts the filter will be set to zero. This is the same as saying glBindTexture(GL_TEXTURE_2D, texture[0]). If we press 'F' once more, the variable filter will equal one, which is the same as saying glBindTexture(GL_TEXTURE_2D, texture[1]). By using the variable filter we can select any of the three textures we've made.   
       

     glBindTexture(GL_TEXTURE_2D, texture[filter]);    // Select A Texture Based On filter

     glBegin(GL_QUADS);       // Start Drawing Quads

       
    glNormal3f is new to my tutorials. A normal is a line pointing straight out of the middle of a polygon at a 90 degree angle. When you use lighting, you need to specify a normal. The normal tells OpenGL which direction the polygon is facing... which way is up. If you don't specify normals, all kinds of weird things happen. Faces that shouldn't light up will light up, the wrong side of a polygon will light up, etc. The normal should point outwards from the polygon.

    Looking at the front face you'll notice that the normal is positive on the z axis. This means the normal is pointing at the viewer. Exactly the direction we want it pointing. On the back face, the normal is pointing away from the viewer, into the screen. Again exactly what we want. If the cube is spun 180 degrees on either the x or y axis, the front will be facing into the screen and the back will be facing towards the viewer. No matter what face is facing the viewer, the normal of that face will also be pointing towards the viewer. Because the light is close to the viewer, any time the normal is pointing towards the viewer it's also pointing towards the light. When it does, the face will light up. The more a normal points towards the light, the brighter that face is. If you move into the center of the cube you'll notice it's dark. The normals are point out, not in, so there's no light inside the box, exactly as it should be.   
       

      // Front Face
      glNormal3f( 0.0f, 0.0f, 1.0f);     // Normal Pointing Towards Viewer
      glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f,  1.0f); // Point 1 (Front)
      glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f,  1.0f); // Point 2 (Front)
      glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f,  1.0f,  1.0f); // Point 3 (Front)
      glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f,  1.0f,  1.0f); // Point 4 (Front)
      // Back Face
      glNormal3f( 0.0f, 0.0f,-1.0f);     // Normal Pointing Away From Viewer
      glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f); // Point 1 (Back)
      glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f,  1.0f, -1.0f); // Point 2 (Back)
      glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f,  1.0f, -1.0f); // Point 3 (Back)
      glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f); // Point 4 (Back)
      // Top Face
      glNormal3f( 0.0f, 1.0f, 0.0f);     // Normal Pointing Up
      glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f,  1.0f, -1.0f); // Point 1 (Top)
      glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f,  1.0f,  1.0f); // Point 2 (Top)
      glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f,  1.0f,  1.0f); // Point 3 (Top)
      glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f,  1.0f, -1.0f); // Point 4 (Top)
      // Bottom Face
      glNormal3f( 0.0f,-1.0f, 0.0f);     // Normal Pointing Down
      glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f); // Point 1 (Bottom)
      glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f); // Point 2 (Bottom)
      glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f,  1.0f); // Point 3 (Bottom)
      glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f,  1.0f); // Point 4 (Bottom)
      // Right face
      glNormal3f( 1.0f, 0.0f, 0.0f);     // Normal Pointing Right
      glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f); // Point 1 (Right)
      glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f,  1.0f, -1.0f); // Point 2 (Right)
      glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f,  1.0f,  1.0f); // Point 3 (Right)
      glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f,  1.0f); // Point 4 (Right)
      // Left Face
      glNormal3f(-1.0f, 0.0f, 0.0f);     // Normal Pointing Left
      glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f); // Point 1 (Left)
      glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f,  1.0f); // Point 2 (Left)
      glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f,  1.0f,  1.0f); // Point 3 (Left)
      glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f,  1.0f, -1.0f); // Point 4 (Left)
     glEnd();        // Done Drawing Quads

       
    The next two lines increase xrot and yrot by the amount stored in xspeed, and yspeed. If the value in xspeed or yspeed is high, xrot and yrot will increase quickly. The faster xrot, or yrot increases, the faster the cube spins on that axis.   
       

     xrot+=xspeed;        // Add xspeed To xrot
     yrot+=yspeed;        // Add yspeed To yrot
     return TRUE;        // Keep Going
    }

       
    Now we move down to WinMain(). Were going to add code to turn lighting on and off, spin the crate, change the filter and move the crate into and out of the screen. Closer to the bottom of WinMain() you will see the command SwapBuffers(hDC). Immediately after this line, add the following code.

    This code checks to see if the letter 'L' has been pressed on the keyboard. The first line checks to see if 'L' is being pressed. If 'L' is being pressed, but lp isn't false, meaning 'L' has already been pressed once or it's being held down, nothing will happen.   
       

        SwapBuffers(hDC);    // Swap Buffers (Double Buffering)
        if (keys['L'] && !lp)    // L Key Being Pressed Not Held?
        {

       
    If lp was false, meaning the 'L' key hasn't been pressed yet, or it's been released, lp becomes true. This forces the person to let go of the 'L' key before this code will run again. If we didn't check to see if the key was being held down, the lighting would flicker off and on over and over, because the program would think you were pressing the 'L' key over and over again each time it came to this section of code.

    Once lp has been set to true, telling the computer that 'L' is being held down, we toggle lighting off and on. The variable light can only be true of false. So if we say light=!light, what we are actually saying is light equals NOT light. Which in english translates to if light equals true make light not true (false), and if light equals false, make light not false (true). So if light was true, it becomes false, and if light was false it becomes true.   
       

         lp=TRUE;    // lp Becomes TRUE
         light=!light;    // Toggle Light TRUE/FALSE

       
    Now we check to see what light ended up being. The first line translated to english means: If light equals false. So if you put it all together, the lines do the following: If light equals false, disable lighting. This turns all lighting off. The command 'else' translates to: if it wasn't false. So if light wasn't false, it must have been true, so we turn lighting on.   
       

         if (!light)    // If Not Light
         {
          glDisable(GL_LIGHTING);  // Disable Lighting
         }
         else     // Otherwise
         {
          glEnable(GL_LIGHTING);  // Enable Lighting
         }
        }

       
    The following line checks to see if we stopped pressing the 'L' key. If we did, it makes the variable lp equal false, meaning the 'L' key isn't pressed. If we didn't check to see if the key was released, we'd be able to turn lighting on once, but because the computer would always think 'L' was being held down so it wouldn't let us turn it back off.   
       

        if (!keys['L'])     // Has L Key Been Released?
        {
         lp=FALSE;    // If So, lp Becomes FALSE
        }

       
    Now we do something similar with the 'F' key. if the key is being pressed, and it's not being held down or it's never been pressed before, it will make the variable fp equal true meaning the key is now being held down. It will then increase the variable called filter. If filter is greater than 2 (which would be texture[3], and that texture doesn't exist), we reset the variable filter back to zero.   
       

        if (keys['F'] && !fp)    // Is F Key Being Pressed?
        {
         fp=TRUE;    // fp Becomes TRUE
         filter+=1;    // filter Value Increases By One
         if (filter>2)    // Is Value Greater Than 2?
         {
          filter=0;   // If So, Set filter To 0
         }
        }
        if (!keys['F'])     // Has F Key Been Released?
        {
         fp=FALSE;    // If So, fp Becomes FALSE
        }

       
    The next four lines check to see if we are pressing the 'Page Up' key. If we are it decreases the variable z. If this variable decreases, the cube will move into the distance because of the glTranslatef(0.0f,0.0f,z) command used in the DrawGLScene procedure.   
       

        if (keys[VK_PRIOR])    // Is Page Up Being Pressed?
        {
         z-=0.02f;    // If So, Move Into The Screen
        }

       
    These four lines check to see if we are pressing the 'Page Down' key. If we are it increases the variable z and moves the cube towards the viewer because of the glTranslatef(0.0f,0.0f,z) command used in the DrawGLScene procedure.   
       

        if (keys[VK_NEXT])    // Is Page Down Being Pressed?
        {
         z+=0.02f;    // If So, Move Towards The Viewer
        }

       
    Now all we have to check for is the arrow keys. By pressing left or right, xspeed is increased or decreased. By pressing up or down, yspeed is increased or decreased. Remember further up in the tutorial I said that if the value in xspeed or yspeed was high, the cube would spin faster. The longer you hold down an arrow key, the faster the cube will spin in that direction.   
       

        if (keys[VK_UP])    // Is Up Arrow Being Pressed?
        {
         xspeed-=0.01f;    // If So, Decrease xspeed
        }
        if (keys[VK_DOWN])    // Is Down Arrow Being Pressed?
        {
         xspeed+=0.01f;    // If So, Increase xspeed
        }
        if (keys[VK_RIGHT])    // Is Right Arrow Being Pressed?
        {
         yspeed+=0.01f;    // If So, Increase yspeed
        }
        if (keys[VK_LEFT])    // Is Left Arrow Being Pressed?
        {
         yspeed-=0.01f;    // If So, Decrease yspeed
        }

       
    Like all the previous tutorials, make sure the title at the top of the window is correct.   
       

        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 Textures, Lighting & Keyboard Tutorial",640,480,16,fullscreen))
         {
          return 0;   // Quit If Window Was Not Created
         }
        }
       }
      }
     }

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

       
    By the end of this tutorial you should be able to create and interact with high quality, realistic looking, textured mapped objects made up of quads. You should understand the benefits of each of the three filters used in this tutorial. By pressing specific keys on the keyboard you should be able to interact with the object(s) on the screen, and finally, you should know how to apply simple lighting to a scene making the scene appear more realistic.

    Jeff Molofee (NeHe)

    ----------------------------------------------
    越学越无知

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/10/13 10:08:00
     
     一分之千 帅哥哟,离线,有人找我吗?射手座1984-11-30
      
      
      威望:1
      等级:研一(随老板参加了WWW大会还和Tim Berners-Lee合了影^_^)
      文章:632
      积分:4379
      门派:XML.ORG.CN
      注册:2006/12/31

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给一分之千发送一个短消息 把一分之千加入好友 查看一分之千的个人资料 搜索一分之千在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看一分之千的博客3
    发贴心情 

    第八课

    按此在新窗口浏览图片混合:

    在这一课里,我们在纹理的基础上加上了混合,它看起具有透明的效果,当然解释它不是那么容易,当希望你喜欢它。

      
       
       
    简单的透明
    OpenGL中的绝大多数特效都与某些类型的(色彩)混合有关。混色的定义为,将某个象素的颜色和已绘制在屏幕上与其对应的象素颜色相互结合。至于如何结合这两个颜色则依赖于颜色的alpha通道的分量值,以及/或者所使用的混色函数。Alpha通常是位于颜色值末尾的第4个颜色组成分量。前面这些课我们都是用GL_RGB来指定颜色的三个分量。相应的GL_RGBA可以指定alpha分量的值。更进一步,我们可以使用glColor4f()来代替glColor3f()。
    绝大多数人都认为Alpha分量代表材料的透明度。这就是说,alpha值为0.0时所代表的材料是完全透明的。alpha值为1.0时所代表的材料则是完全不透明的。
    混色的公式
    若您对数学不感冒,而只想看看如何实现透明,请跳过这一节。若您想深入理解(色彩)混合的工作原理,这一节应该适合您吧。『译者注:其实并不难^-^。原文中的公式如下,CKER再唠叨一下吧。其实混合的基本原理是就将要分色的图像各象素的颜色以及背景颜色均按照RGB规则各自分离之后,根据-图像的RGB颜色分量*alpha值+背景的RGB颜色分量*(1-alpha值)-这样一个简单公式来混合之后,最后将混合得到的RGB分量重新合并。』
    公式如下:
    (Rs Sr + Rd Dr, Gs Sg + Gd Dg, Bs Sb + Bd Db, As Sa + Ad Da)
    OpenGL按照上面的公式计算这两个象素的混色结果。小写的s和r分别代表源象素和目标象素。大写的S和D则是相应的混色因子。这些决定了您如何对这些象素混色。绝大多数情况下,各颜色通道的alpha混色值大小相同,这样对源象素就有 (As, As, As, As),目标象素则有1, 1, 1, 1) - (As, As, As, As)。上面的公式就成了下面的模样:
    (Rs As + Rd (1 - As), Gs As + Gd (1 - As), Bs As + Bs (1 - As), As As + Ad (1 - As))
    这个公式会生成透明/半透明的效果。

    OpenGL中的混色
    在OpenGL中实现混色的步骤类似于我们以前提到的OpenGL过程。接着设置公式,并在绘制透明对象时关闭写深度缓存。因为我们想在半透明的图形背后绘制 对象。这不是正确的混色方法,但绝大多数时候这种做法在简单的项目中都工作的很好。
    Rui Martins 的补充: 正确的混色过程应该是先绘制全部的场景之后再绘制透明的图形。并且要按照与深度缓存相反的次序来绘制(先画最远的物体)。
    考虑对两个多边形(1和2)进行alpha混合,不同的绘制次序会得到不同的结果。(这里假定多边形1离观察者最近,那么正确的过程应该先画多边形2,再画多边形1。正如您再现实中所见到的那样,从这两个<透明的>多边形背后照射来的光线总是先穿过多边形2,再穿过多边形1,最后才到达观察者的眼睛。)
    在深度缓存启用时,您应该将透明图形按照深度进行排序,并在全部场景绘制完毕之后再绘制这些透明物体。否则您将得到不正确的结果。我知道某些时候这样做是很令人痛苦的,但这是正确的方法。
    我们将使用第七课的代码。一开始先在代码开始处增加两个新的变量。出于清晰起见,我重写了整段代码。

      
       

    bool    blend;      // 是否混合?
    bool bp;      // B 键按下了么?

       
    然后往下移动到 LoadGLTextures() 这里。找到" if (TextureImage[0]=LoadBMP("Data/Crate.bmp")) "这一行。我们现在使用有色玻璃纹理来代替上一课中的木箱纹理。  
       

     if (TextureImage[0]=LoadBMP("Data/glass.bmp")) // 载入玻璃位图

       
    在InitGL()代码段加入以下两行。第一行以全亮度绘制此物体,并对其进行50%的alpha混合(半透明)。当混合选项打开时,此物体将会产生50%的透明效果。第二行设置所采用的混合类型。
    Rui Martins 的补充: alpha通道的值为 0.0意味着物体材质是完全透明的。1.0 则意味着完全不透明。   
       

     glColor4f(1.0f,1.0f,1.0f,0.5f);   // 全亮度, 50% Alpha 混合
     glBlendFunc(GL_SRC_ALPHA,GL_ONE);  // 基于源象素alpha通道值的半透明混合函数

       
    在接近第七课结尾处的地方找到下面的代码段。  
       

     if (keys[VK_LEFT])    // Left方向键按下了么?
     {
      yspeed-=0.01f;    // 若是, 减少yspeed
     }

       
    接着上面的代码,我们增加如下的代码。这几行监视B键是否按下。如果是的话,计算机检查混合选项是否已经打开。然后将其置为相反的状态。  
       

     if (keys['B'] && !bp)    // B 健按下且bp为 FALSE么?
     {
      bp=TRUE;    // 若是, bp 设为 TRUE
      blend = !blend;    // 切换混合选项的 TRUE / FALSE
      if(blend)    // 混合打开了么?
      {
       glEnable(GL_BLEND);  // 打开混合
       glDisable(GL_DEPTH_TEST); // 关闭深度测试
      }
      else     // 否则
      {
       glDisable(GL_BLEND);  // 关闭混合
       glEnable(GL_DEPTH_TEST); // 打开深度测试
      }
     }
     if (!keys['B'])     //  B 键松开了么?
     {
      bp=FALSE;    // 若是, bp设为 FALSE
     }

       
    但是怎样才能在使用纹理贴图的时候指定混合时的颜色呢?很简单,在调整贴图模式时,文理贴图的每个象素点的颜色都是由alpha通道参数与当前地象素颜色相乘所得到的。比如,绘制的颜色是 (0.5, 0.6, 0.4),我们会把颜色相乘得到(0.5, 0.6, 0.4, 0.2) (alpha参数在没有指定时,缺省为零)。
    就是如此!OpenGL实现Alpha混合的确很简单!

    原文注 (11/13/99)
    我(NeHe)混色代码进行了修改,以使显示的物体看起来更逼真。同时对源象素和目的象素使用alpha参数来混合,会导致物体的人造痕迹看起来很明显。 会使得物体的背面沿着侧面的地方显得更暗。基本上物体会看起来很怪异。我所用的混色方法也许不是最好的,但的确能够工作。启用光源之后,物体看起来很逼真。感谢Tom提供的原始代码,他采用的混色方法是正确的,但物体看起来并不象所期望的那样吸引人:)
    代码所作的再次修改是因为在某些显卡上glDepthMask()函数存在寻址问题。这条命令在某些卡上启用或关闭深度缓冲测试时似乎不是很有效,所以我已经将启用或关闭深度缓冲测试的代码转成老式的glEnable和glDisable。

    纹理贴图的Alpha混合
    用于纹理贴图的alpha参数可以象颜色一样从问题贴图中读取。方法如下,您需要在载入所需的材质同时取得其的alpha参数。然后在调用glTexImage2D()时使用GL_RGBA的颜色格式。

    ----------------------------------------------
    越学越无知

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/10/13 10:11:00
     
     一分之千 帅哥哟,离线,有人找我吗?射手座1984-11-30
      
      
      威望:1
      等级:研一(随老板参加了WWW大会还和Tim Berners-Lee合了影^_^)
      文章:632
      积分:4379
      门派:XML.ORG.CN
      注册:2006/12/31

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给一分之千发送一个短消息 把一分之千加入好友 查看一分之千的个人资料 搜索一分之千在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看一分之千的博客4
    发贴心情 
    Lesson 08
       
    Simple Transparency

    Most special effects in OpenGL rely on some type of blending. Blending is used to combine the color of a given pixel that is about to be drawn with the pixel that is already on the screen. How the colors are combined is based on the alpha value of the colors, and/or the blending function that is being used. Alpha is a 4th color component usually specified at the end. In the past you have used GL_RGB to specify color with 3 components. GL_RGBA can be used to specify alpha as well. In addition, we can use glColor4f() instead of glColor3f().

    Most people think of Alpha as how opaque a material is. An alpha value of 0.0 would mean that the material is completely transparent. A value of 1.0 would be totally opaque.

    The Blending Equation

    If you are uncomfortable with math, and just want to see how to do transparency, skip this section. If you want to understand how blending works, this section is for you.

    (Rs Sr + Rd Dr, Gs Sg + Gd Dg, Bs Sb + Bd Db, As Sa + Ad Da)

    OpenGL will calculate the result of blending two pixels based on the above equation. The s and r subscripts specify the source and destination pixels. The S and D components are the blend factors. These values indicate how you would like to blend the pixels. The most common values for S and D are (As, As, As, As) (AKA source alpha) for S and (1, 1, 1, 1) - (As, As, As, As) (AKA one minus src alpha) for D. This will yield a blending equation that looks like this:

    (Rs As + Rd (1 - As), Gs As + Gd (1 - As), Bs As + Bs (1 - As), As As + Ad (1 - As))

    This equation will yield transparent/translucent style effects.

    Blending in OpenGL

    We enable blending just like everything else. Then we set the equation, and turn off depth buffer writing when drawing transparent objects, since we still want objects behind the translucent shapes to be drawn. This isn't the proper way to blend, but most the time in simple projects it will work fine. Rui Martins Adds: The correct way is to draw all the transparent (with alpha < 1.0) polys after you have drawn the entire scene, and to draw them in reverse depth order (farthest first). This is due to the fact that blending two polygons (1 and 2) in different order gives different results, i.e. (assuming poly 1 is nearest to the viewer, the correct way would be to draw poly 2 first and then poly 1. If you look at it, like in reality, all the light comming from behind these two polys (which are transparent) has to pass poly 2 first and then poly 1 before it reaches the eye of the viewer. You should SORT THE TRANSPARENT POLYGONS BY DEPTH and draw them AFTER THE ENTIRE SCENE HAS BEEN DRAWN, with the DEPTH BUFFER ENABLED, or you will get incorrect results. I know this sometimes is a pain, but this is the correct way to do it.

    We'll be using the code from the last tutorial. We start off by adding two new variables to the top of the code. I'll rewrite the entire section of code for clarity.   
       

    #include <windows.h>     // Header File For Windows
    #include <stdio.h>     // Header File For Standard Input/Output
    #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

    HDC  hDC=NULL;    // Private GDI Device Context
    HGLRC  hRC=NULL;    // Permanent Rendering Context
    HWND  hWnd=NULL;    // Holds Our Window Handle
    HINSTANCE hInstance;    // Holds The Instance Of The Application

    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
    bool light;      // Lighting ON/OFF
    bool    blend;      // Blending OFF/ON? ( NEW )
    bool lp;      // L Pressed?
    bool fp;      // F Pressed?
    bool bp;      // B Pressed? ( NEW )

    GLfloat xrot;      // X Rotation
    GLfloat yrot;      // Y Rotation
    GLfloat xspeed;      // X Rotation Speed
    GLfloat yspeed;      // Y Rotation Speed

    GLfloat z=-5.0f;     // Depth Into The Screen

    GLfloat LightAmbient[]=  { 0.5f, 0.5f, 0.5f, 1.0f }; // Ambient Light Values
    GLfloat LightDiffuse[]=  { 1.0f, 1.0f, 1.0f, 1.0f }; // Diffuse Light Values
    GLfloat LightPosition[]= { 0.0f, 0.0f, 2.0f, 1.0f }; // Light Position

    GLuint filter;      // Which Filter To Use
    GLuint texture[3];     // Storage for 3 textures

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

       
    Move down to LoadGLTextures(). Find the line that says: if (TextureImage[0]=LoadBMP("Data/Crate.bmp")). Change it to the line below. We're using a stained glass type texture for this tutorial instead of the crate texture.   
       

     if (TextureImage[0]=LoadBMP("Data/glass.bmp")) // Load The Glass Bitmap ( MODIFIED )

       
    Add the following two lines somewhere in the InitGL() section of code. What this line does is sets the drawing brightness of the object to full brightness with 50% alpha (opacity). This means when blending is enabled, the object will be 50% transparent. The second line sets the type of blending we're going to use.

    Rui Martins Adds: An alpha value of 0.0 would mean that the material is completely transparent. A value of 1.0 would be totally opaque.   
       

     glColor4f(1.0f,1.0f,1.0f,0.5f);   // Full Brightness, 50% Alpha ( NEW )
     glBlendFunc(GL_SRC_ALPHA,GL_ONE);  // Blending Function For Translucency Based On Source Alpha Value ( NEW )

       
    Look for the following section of code, it can be found at the very bottom of lesson seven.   
       

     if (keys[VK_LEFT])    // Is Left Arrow Being Pressed?
     {
      yspeed-=0.01f;    // If So, Decrease yspeed
     }

       
    Right under the above code, we want to add the following lines. The lines below watch to see if the 'B' key has been pressed. If it has been pressed, the computer checks to see if blending is off or on. If blending is on, the computer turns it off. If blending was off, the computer will turn it on.   
       

     if (keys['B'] && !bp)    // Is B Key Pressed And bp FALSE?
     {
      bp=TRUE;    // If So, bp Becomes TRUE
      blend = !blend;    // Toggle blend TRUE / FALSE 
      if(blend)    // Is blend TRUE?
      {
       glEnable(GL_BLEND);  // Turn Blending On
       glDisable(GL_DEPTH_TEST); // Turn Depth Testing Off
      }
      else     // Otherwise
      {
       glDisable(GL_BLEND);  // Turn Blending Off
       glEnable(GL_DEPTH_TEST); // Turn Depth Testing On
      }
     }
     if (!keys['B'])     // Has B Key Been Released?
     {
      bp=FALSE;    // If So, bp Becomes FALSE
     }

       
    But how can we specify the color if we are using a texture map? Simple, in modulated texture mode, each pixel that is texture mapped is multiplied by the current color. So, if the color to be drawn is (0.5, 0.6, 0.4), we multiply it times the color and we get (0.5, 0.6, 0.4, 0.2) (alpha is assumed to be 1.0 if not specified).

    Thats it! Blending is actually quite simple to do in OpenGL.

    Note (11/13/99)

    I ( NeHe ) have modified the blending code so the output of the object looks more like it should. Using Alpha values for the source and destination to do the blending will cause artifacting. Causing back faces to appear darker, along with side faces. Basically the object will look very screwy. The way I do blending may not be the best way, but it works, and the object appears to look like it should when lighting is enabled. Thanks to Tom for the initial code, the way he was blending was the proper way to blend with alpha values, but didn't look as attractive as people expected :)

    The code was modified once again to address problems that some video cards had with glDepthMask(). It seems this command would not effectively enable and disable depth buffer testing on some cards, so I've changed back to the old fashioned glEnable and Disable of Depth Testing.

    Alpha From Texture Map.

    The alpha value that is used for transparency can be read from a texture map just like color, to do this, you will need to get alpha into the image you want to load, and then use GL_RGBA for the color format in calls to glTexImage2D().

    Questions?

    If you have any questions, feel free to contact me at stanis@cs.wisc.edu.

    Tom Stanis

    Jeff Molofee (NeHe)

    ----------------------------------------------
    越学越无知

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/10/13 10:12:00
     
     walthouse 帅哥哟,离线,有人找我吗?
      
      
      等级:大一新生
      文章:0
      积分:55
      门派:XML.ORG.CN
      注册:2007/4/25

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给walthouse发送一个短消息 把walthouse加入好友 查看walthouse的个人资料 搜索walthouse在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看walthouse的博客5
    发贴心情 
    学习了,谢谢楼主.
    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2008/2/22 9:35:00
     
     freewind 帅哥哟,离线,有人找我吗?
      
      
      等级:大一新生
      文章:2
      积分:58
      门派:XML.ORG.CN
      注册:2010/5/31

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给freewind发送一个短消息 把freewind加入好友 查看freewind的个人资料 搜索freewind在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看freewind的博客6
    发贴心情 
    好像最后一段翻译有个小问题吧   缺省应该是1.0f即不透明 还是0.0f?(alpha is assumed to be 1.0 if not specified). (alpha参数在没有指定时,缺省为零)。
    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2010/7/12 12:02:00
     
     GoogleAdSense
      
      
      等级:大一新生
      文章:1
      积分:50
      门派:无门无派
      院校:未填写
      注册:2007-01-01
    给Google AdSense发送一个短消息 把Google AdSense加入好友 查看Google AdSense的个人资料 搜索Google AdSense在『 C/C++编程思想 』的所有贴子 访问Google AdSense的主页 引用回复这个贴子 回复这个贴子 查看Google AdSense的博客广告
    2024/9/7 19:09:18

    本主题贴数6,分页: [1]

    管理选项修改tag | 锁定 | 解锁 | 提升 | 删除 | 移动 | 固顶 | 总固顶 | 奖励 | 惩罚 | 发布公告
    W3C Contributing Supporter! W 3 C h i n a ( since 2003 ) 旗 下 站 点
    苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
    3,296.875ms