C++博客 :: 首页 :: 联系 ::  :: 管理
  163 Posts :: 4 Stories :: 350 Comments :: 0 Trackbacks

常用链接

留言簿(48)

我参与的团队

搜索

  •  

积分与排名

  • 积分 - 405802
  • 排名 - 60

最新评论

阅读排行榜

评论排行榜

 欢迎进入第九课。到现在为止,您应该很好的理解OpenGL了。您已经学会了设置一个OpenGL窗口的每个细节。学会在旋转的物体上贴图并打上光线以及混色(透明)处理。这一课应该算是第一课中级教程。 您将学到如下的知识:在3D场景中移动位图,并去除位图上的黑色象素(使用混色)。接着为黑白纹理上色,最后您将学会创建丰富的色彩,并把上过不同色彩的 纹理相互混合,得到简单的动画效果。我们在第一课的代码基础上进行修改。先在程序源码的开始处增加几个变量。出于清晰起见,我重写了整段代码。

  #include <windows.h>                    // Windows的头文件
  #include <stdio.h>                     // 标准输入/输出库的头文件
  #include <gl\\gl.h>                     // OpenGL32库的头文件
  #include <gl\\glu.h>                    // GLu32库的头文件
  #include <gl\\glaux.h>                   // GLaux库的头文件

  HGLRC hRC=NULL;                      // 永久着色描述表
  HDC hDC=NULL;                       // 私有GDI设备描述表
  HWND hWnd=NULL;                      // 保存我们的窗口句柄
  HINSTANCE hInstance;                    // 保存程序的实例

  bool keys[256];                      // 用于键盘例程的数组
  bool active=TRUE;                     // 窗口的活动标志,缺省为TRUE
  bool fullscreen=TRUE;                   // 全屏标志缺省设定成全屏模式

   下列这几行新加的。twinkletp是布尔变量,表示它们只能设为TRUEFALSEtwinkle用来跟踪“闪烁”效果是否启用。tp用来检查‘T’键有没有被按下或松开。(按下时tp=TRUE,松开时tp=FALSE

  BOOL twinkle;                       // 闪烁的星星
  BOOL tp;                          // \’T\’ 按下了么?

  num跟踪屏幕上所绘制的星星数。这个数字被定义为一个常量。这意味着无法在以后的代码中对其进行修改。这么做的原因是因为您无法重新定义一个数组。因此,如果我们定义一个50颗星星的数组,然后又将num增加到51的话,就会出错(CKER:数组越界)。不过您还是可以(也只可以)在这一行上随意修改这个数字。但是以后请您别再改动num的值了,除非您想看见灾难发生。

  const num=50;                       // 绘制的星星数

   现在我们来创建一个结构。结构这词听起来有点可怕,但实际上并非如此。一个结构使用一组简单类型的数据 (以及变量等)来表达较大的具有相似性的数据组合。我们知道我们在保持对星星的跟踪。您可以看到下面的第七行就是stars;并且每个星星有三个整型的色彩值。第三行intr,g,b设置了三个整数。一个红色(r)、一个绿色(g)、以及一个蓝色(b)。此外,每个星星离屏幕中心的距离不同,而且可以是以屏幕中心为原点的任意360度中的一个角度。如果你看下面第四行的话,会发现我们使用了一个叫做dist的浮点数来保持对距离的跟踪。第五行则用一个叫做angle的浮点数保持对星星角度值的跟踪。
   因此我们使用了一组数据来描述屏幕上星星的色彩,距离,及角度。不幸的是我们不止对一个星星进行跟踪。但是无需创建50个红色值、50个绿色值、50个蓝色值、50个距离值,以及50个角度值,而只需创建一个数组starstar数组的每个元素都是stars类型的,里面存放了描述星星的所有数据。star数组在下面的第八行创建。 第八行的样子是这样的:stars star[num]。数组类型是stars结构。所数组能存放所有stars结构的信息。数组名字是star。数组大小是[num]。数组中存放着stars结构的元素。跟踪结构元素会比跟踪各自分开的变量容易的多。不过这样也很笨,因为我们竟然不能改变常量num来增减星星数量。

  typedef struct                       // 为星星创建一个结构
  {
     int r, g, b;
                      // 星星的颜色
    GLfloat dist;                     // 星星距离中心的距离
    GLfloat angle;                     // 当前星星所处的角度
  }
   stars;
                           // 结构命名为stars
  stars star[num];                      // 使用 \’stars\’ 结构生成一个
                                 // 包含\’num\’个元素的 \’star\’数组

   接下来我们设置几个跟踪变量:星星离观察者的距离变量(zoom),我们所见到的星星所处的角度(tilt),以及使闪烁的星星绕Z轴自转的变量spin
  loop变量用来绘制50颗星星。texture[1]用来存放一个黑白纹理。如果您需要更多的纹理的话,您应该增加texture数组的大小至您决定采用的纹理个数。

  GLfloat zoom=-15.0f;                    // 星星离观察者的距离
  GLfloat tilt=90.0f;                    // 星星的倾角
  GLfloat spin;                       // 闪烁星星的自转
  GLuint loop;                        // 全局 Loop 变量
  GLuint texture[1];                     // 存放一个纹理

  LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);   // WndProc的声明

   紧接着上面的代码就是我们用来载入纹理的代码。我不打算再详细的解释这段代码。这跟我们在第六、七、八课中所用的代码是一模一样的。这次载入的位图叫做“star.bmp”。这里我们使用glGenTextures(1,&texture[0]),来生成一个纹理。纹理采用线性滤波方式。

  AUX_RGBImageRec *LoadBMP(char *Filename)          // 载入位图
  {
       FILE *File=NULL;
                  // 文件句柄
      if (!Filename)                   // 确认文件名已初始化
      {
           return NULL;
                // 没有返回 NULL
      }
       File=fopen(Filename,"r");
             // 检查文件是否存在
      if (File)                     // 文件存在么?
      {
           fclose(File); 
              // 关闭文件句柄
          return auxDIBImageLoad(Filename);     // 载入位图并返回一个指针
      }
      return NULL;                    // 载入失败返回 NULL
  }

   下面的代码(调用上面的代码)载入位图,并转换成纹理。变量用来跟踪纹理是否已载入并创建好了。

  int LoadGLTextures()                    // 载入位图并转换成纹理
  {
       int Status=FALSE;
                 // Status 状态指示器
      AUX_RGBImageRec *TextureImage[1];         // 为纹理分配存储空间
      memset(TextureImage,0,sizeof(void *)*1);      // 将指针设为 NULL

       // 载入位图,查错,如果未找到位图文件则退出
      if (TextureImage[0]=LoadBMP("Data/Star.bmp"))
       {
            Status=TRUE;
               // 将 Status 设为TRUE
           glGenTextures(1, &texture[0]);      // 创建一个纹理

            // 创建一个线性滤波纹理
           glBindTexture(GL_TEXTURE_2D, texture[0]);
            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);
       }

       if (TextureImage[0])
                // 如果纹理存在
      {
          if (TextureImage[0]->data)
          // 如果纹理图像存在
         {
              free(TextureImage[0]->data);
     // 释放纹理图像所占的内存
         }
          free(TextureImage[0]);
            // 释放图像结构
      }
       return Status;
                   // 返回 Status的值
  }

现在设置OpenGL的渲染方式。这里不打算使用深度测试,如果您使用第一课的代码的话,请确认是否已经去掉了glDepthFunc (GL_LEQUAL);和glEnable(GL_DEPTH_TEST);两行。否则,您所见到的效果将会一团糟。这里我们使用了纹理映射,因此请您 确认您已经加上了这些第一课中所没有的代码。您会注意到我们通过混色来启用了纹理映射。

  int InitGL(GLvoid)                     // 此处开始对OpenGL进行所有设置
  {
       if (!LoadGLTextures())
               // 调用纹理载入子例程
      {
           return FALSE;
               // 如果未能载入,返回FALSE
      }

       glEnable(GL_TEXTURE_2D);
              // 启用纹理映射
      glShadeModel(GL_SMOOTH);              // 启用阴影平滑
      glClearColor(0.0f, 0.0f, 0.0f, 0.5f);       // 黑色背景
      glClearDepth(1.0f);                // 设置深度缓存
      glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // 真正精细的透视修正
      glBlendFunc(GL_SRC_ALPHA,GL_ONE);         // 设置混色函数取得半透明效果
      glEnable(GL_BLEND);                // 启用混色

   以下是新增的代码。设置了每颗星星的起始角度、距离、和颜色。您会注意到修改结构的属性有多容易。全部50颗星星都会被循环设置。要改变star[1]的角度我们所要做的只是star[1].angle={某个数值};就这么简单。

      for (loop=0; loop<num; loop++)           // 创建循环设置全部星星
      {
           star[loop].angle=0.0f;
           // 所有星星都从零角度开始

   第loop颗星星离中心的距离是将loop的值除以星星的总颗数,然后乘上5.0f。基本上这样使得后一颗星星比前一颗星星离中心更远一点。这样当loop为50时(最后一颗星星),loop除以num正 好是1.0f。之所以要乘以5.0f是因为1.0f*5.0f 就是 5.0f。5.0f已经很接近屏幕边缘。我不想星星飞出屏幕,5.0f是最好的选择了。当然如果如果您将场景设置的更深入屏幕里面的话,也许可以使用大于 5.0f的数值,但星星看起来就更小一些(都是透视的缘故)。
您还会注意到每颗星星的颜色都是从0~255之间的一个随机数。也许您会奇怪为何这里的颜色得取值范围不是OpenGL通常的0.0f~1.0f之间。这 里我们使用的颜色设置函数是glColor4ub,而不是以前的glColor4f。ub意味着参数是Unsigned Byte型的。一个byte的取值范围是0~255。这里使用byte值取随机整数似乎要比取一个浮点的随机数更容易一些。

          star[loop].dist=(float(loop)/num)*5.0f;  // 计算星星离中心的距离
          star[loop].r=rand()%256;          // 为star[loop]设置随机红色分量
          star[loop].g=rand()%256;          // 为star[loop]设置随机红色分量
          star[loop].b=rand()%256;          // 为star[loop]设置随机红色分量
      }
       return TRUE;
                    // 初始化一切OK
  }

   Resize的代码也是一样的,现在我们转入绘图代码。如果您使用第一课的代码,删除旧的DrawGLScene代码,只需将下面的代码复制过去就行了。实际上,第一课的代码只有两行,所以没太多东西要删掉的。

  int DrawGLScene(GLvoid)                  // 此过程中包括所有的绘制代码
  {
       glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 清除屏幕及深度缓存
      glBindTexture(GL_TEXTURE_2D, texture[0]);     // 选择纹理
      for (loop=0; loop<num; loop++)           // 循环设置所有的星星
      {
           glLoadIdentity();
             // 绘制每颗星之前,重置模型观察矩阵
          glTranslatef(0.0f,0.0f,zoom);       // 深入屏幕里面(使用 \’zoom\’的值)
          glRotatef(tilt,1.0f,0.0f,0.0f);      // 倾斜视角(使用\’tilt\’的值)

现在我们来移动星星。星星开始时位于屏幕的中心。我们要做的第一件事是把场景沿Y轴旋转。如果我们旋转90度的话,X轴不再是自左至右的了,他将由里向外 穿出屏幕。为了让大家更清楚些,举个例子。假想您站在房子中间。再设想您左侧的墙上写着-x,前面的墙上写着-z,右面墙上就是+x咯,您身后的墙上则是 +z。加入整个房子向右转90度,但您没有动,那么前面的墙上将是-x而不再是-z了。所有其他的墙也都跟着移动。-z出现在右侧,+z出现在左侧,+x 出现在您背后。神经错乱了吧?通过旋转场景,我们改变了x和z平面的方向。
第二行代码沿x轴移动一个正值。通常x轴上的正值代表移向了屏幕的右侧(也就是通常的x轴的正向),但这里由于我们绕y轴旋转了坐标系,x轴的正向可以是 任意方向。如果我们转180度的话,屏幕的左右侧就镜像反向了。因此,当我们沿 x轴正向移?保赡芟蜃螅蛴遥蚯盎蛳蚝蟆?

          glRotatef(star[loop].angle,0.0f,1.0f,0.0f);// 旋转至当前所画星星的角度
          glTranslatef(star[loop].dist,0.0f,0.0f);  // 沿X轴正向移动

接着的代码带点小技巧。星星实际上是一个平面的纹理。现在您在屏幕中心画了个平面的四边形然后贴上纹理,这看起来很不错。一切都如您所想的那样。但是当您 当您沿着y轴转上个90度的话,纹理在屏幕上就只剩右侧和左侧的两条边朝着您。看起来就是一条细线。这不是我们所想要的。我们希望星星永远正面朝着我们, 而不管屏幕如何旋转或倾斜。
我们通过在绘制星星之前,抵消对星星所作的任何旋转来实现这个愿望。您可以采用逆序来抵消旋转。当我们倾斜屏幕时,我们实际上以当前角度旋转了星星。通过 逆序,我们又以当前角度“反旋转”星星。也就是以当前角度的负值来旋转星星。就是说,如果我们将星星旋转了10度的话,又将其旋转-10度来使星星在那个 轴上重新面对屏幕。下面的第一行抵消了沿y轴的旋转。然后,我们还需要抵消掉沿x轴的屏幕倾斜。要做到这一点,我们只需要将屏幕再旋转-tilt倾角。在 抵消掉x和y轴的旋转后,星星又完全面对着我们了。

          glRotatef(-star[loop].angle,0.0f,1.0f,0.0f);// 取消当前星星的角度
          glRotatef(-tilt,1.0f,0.0f,0.0f);      // 取消屏幕倾斜

   如果twinkle为 TRUE,我们在屏幕上先画一次不旋转的星星:将星星总数(num)减去当前的星星数(loop)再减去1,来提取每颗星星的不同颜色(这么做是因为循环范围从0到num-1)。举例来说,结果为10的时候,我们就使用10号星星的颜色。这样相邻星星的颜色总是不同的。这不是个好法子,但很有效。最后一个值是alpha通道分量。这个值越小,这颗星星就越暗。
   由于启用了twinkle,每颗星星最后会被绘制两遍。程序运行起来会慢一些,这要看您的机器性能如何了。但两遍绘制的星星颜色相互融合,会产生很棒的效果。同时由于第一遍的星星没有旋转,启用twinkle后 的星星看起来有一种动画效果。(假如您这里看不懂得话,就自己去看程序的运行效果吧。)值得注意的是给纹理上色是件很容易的事。尽管纹理本身是黑白的,纹 理将变成我们在绘制它之前选定的任意颜色。此外,同样值得注意的是我们在这里使用的颜色值是byte型的,而不是通常的浮点数。甚至alpha通道分量也 是如此。

          if (twinkle)                // 启用闪烁效果
          {
              // 使用byte型数值指定一个颜色
              glColor4ub(star[(num-loop)-1].r,
                    star[(num-loop)-1].g,
                    star[(num-loop)-1].b,255);
               glBegin(GL_QUADS);
         // 开始绘制纹理映射过的四边形
                  glTexCoord2f(0.0f, 0.0f);glVertex3f(-1.0f,-1.0f, 0.0f);
                   glTexCoord2f(1.0f, 0.0f);glVertex3f( 1.0f,-1.0f, 0.0f);
                   glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 0.0f);
                   glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 0.0f);
               glEnd();
              // 四边形绘制结束
          }

   现在绘制第二遍的星星。唯一和前面的代码不同的是这一遍的星星肯定会被绘制,并且这次的星星绕着z轴旋转。

          glRotatef(spin,0.0f,0.0f,1.0f);       // 绕z轴旋转星星
          // 使用byte型数值指定?桓鲅丈?
          glColor4ub(star[loop].r,star[loop].g,star[loop].b,255);
           glBegin(GL_QUADS);
             // 开始绘制纹理映射过的四边形
              glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f,-1.0f, 0.0f);
               glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f,-1.0f, 0.0f);
               glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 0.0f);
               glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 0.0f);
           glEnd();
                  // 四边形绘制结束

   以下的代码代表星星的运动。我们增加spin的值来旋转所有的星星(公转)。然后,将每颗星星的自转角度增加loop/num。这使离中心更远的星星转的更快。最后减少每颗星星离屏幕中心的距离。这样看起来,星星们好像被不断地吸入屏幕的中心。

          spin+=0.01f;                // 星星的公转
          star[loop].angle+=float(loop)/num;     // 改变星星的自转角度
          star[loop].dist-=0.01f;          // 改变星星离中心的距离

   接着几行检查星星是否已经碰到了屏幕中心。当星星碰到屏幕中心时,我们为它赋一个新颜色,然后往外移5个单位,这颗星星将踏上它回归屏幕中心的旅程。

          if (star[loop].dist<0.0f)         // 星星到达中心了么
          {
               star[loop].dist+=5.0f;
       // 往外移5个单位
              star[loop].r=rand()%256;      // 赋一个新红色分量
              star[loop].g=rand()%256;      // 赋一个新绿色分量
              star[loop].b=rand()%256;      // 赋一个新蓝色分量
          }
       }
       return TRUE;
                    // 一切正常
  }

   现在我们添加监视键盘的代码。下移到WinMain()。找到SwapBuffers(hDC)一行。我们就在这一行后面增加键盘监视代码。
   代码将检查T键是否已按下。如果T键按下过,并且又放开了,if块内的代码将被执行。如果twinkle为FALSE,他将变为TRUE。反之亦然。只要T键按下,tp就变为TRUE。这样处理可以防止如果您一直按着T键的话,块内的代码被反复执行。

                      SwapBuffers(hDC);  // 切换缓冲 (双缓冲)
                      if (keys[\’T\’] && !tp)// 是否T键已按下并且tp值为 FALSE
                      {
                            tp=TRUE;
  // 若是,将tp设为TRUE
                           twinkle=!twinkle;// 翻转twinkle的值
                      }

   下面的代码检查是否松开了T键。若是,使tp=FALSE。除非tp的值为FALSE,否则按着T键时什么也不会发生。所以这行代码很重要。

                      if (!keys[\’T\’])   // T 键已松开了么?
                      {
                            tp=FALSE;
  // 若是,tp为 FALSE
                      }

   余下的代码检查上、下方向键,向上翻页键或向下翻页键是否按下。

                      if (keys[VK_UP])   // 上方向键按下了么?
                      {
                            tilt-=0.5f;
 // 屏幕向上倾斜
                      }
                       if (keys[VK_DOWN])
  // 下方向键按下了么?
                      {
                            tilt+=0.5f;
 // 屏幕向下倾斜
                      }
                       if (keys[VK_PRIOR])
 // 向上翻页键按下了么
                      {
                            zoom-=0.2f;
 // 缩小
                      }
                       if (keys[VK_NEXT])
  // 向下翻页键按下了么?
                      {
                            zoom+=0.2f;
 // 放大
                      }

   像以前一样,确认窗口的标题是否正确。

                      if (keys[VK_F1])        // F1键按下了么?
                      {
                            keys[VK_F1]=FALSE;

                          KillGLWindow();    // 销毁当前的窗口
                          fullscreen=!fullscreen;// 切换 全屏/窗口 模式
                           // 重建 OpenGL 窗口(修改)
                          if (!CreateGLWindow("NeHe\’s Textures, Lighting & Keyboard Tutorial",640,480,16,fullscreen))
                           {
                                return 0;
// 如果窗口未能创建,程序退出
                          }
                       }

                   }
               }
            }


这一课我尽我所能来解释如何加载一个灰阶位图纹理,(使用混色)去掉它的背景色后,再给它上色,最后让它在3D场景中移动。我已经向您展示了如何创建漂亮 的颜色与动画效果。实现原理是在原始位图上再重叠一份位图拷贝。到现在为止,只要您很好的理解了我所教您的一切,您应该已经能够毫无问题的制作您自己的 3D Demo了。所有的基础知识都已包括在内。
posted on 2007-12-12 19:11 sdfasdf 阅读(941) 评论(0)  编辑 收藏 引用 所属分类: OPENGL