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

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

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

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


    第九课第十课源码下载


    第九课  


    按此在新窗口浏览图片3D空间中移动图像:

    你想知道如何在3D空间中移动物体,你想知道如何在屏幕上绘制一个图像,而让图像的背景色变为透明,你希望有一个简单的动画。这一课将教会你所有的一切。前面的课程涵盖了基础的OpenGL,每一课都是在前一课的基础上创建的。这一课是前面几课知识的综合,当你学习这课时,请确保你已经掌握了前面几课的知识。

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

    #include <stdio.h>     // 标准输入输出库头文件
    #include <glaux.h>     // GLaux库的头文件

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

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

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

       
    现在我们来创建一个结构。 结构这词听起来有点可怕,但实际上并非如此。 一个结构使用一组简单类型的数据 (以及变量等)来表达较大的具有相似性的数据组合。 我们知道我们在保持对星星的跟踪。 您可以看到下面的第七行就是 stars;并且每个星星有三个整型的色彩值。第三行 int r,g,b设置了三个整数. 一个红色 (r), 一个绿色 (g), 以及一个蓝色 (b). 此外,每个星星离屏幕中心的距离不同, 而且可以是以屏幕中心为原点的任意360度中的一个角度。如果你看下面第四行的话, 会发现我们使用了一个叫做 dist的浮点数来保持对距离 的跟踪. 第五行则用一个叫做 angle的浮点数保持对星星角度值的跟踪。
    因此我们使用了一组数据来描述屏幕上星星的色彩, 距离, 和角度。 不幸的是我们不止对一个星星进行跟踪。但是无需创建 50 个红色值、 50 个绿色值、 50 个蓝色值、 50 个距离值 以及 50 个角度值,而只需创建一个数组star。 star数组的每个元素都是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];      // 存放一个纹理

       
    紧接着上面的代码就是我们用来载入纹理的代码。我不打算再详细的解释这段代码。这跟我们在第六、七、八课中所用的代码是一模一样的。这一次载入的位图叫做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;     // 状态指示器

     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。『CKER:废话,废话!这老外怎么跟孔乙己似的!:)』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);   // 深入屏幕里面
      glRotatef(tilt,1.0f,0.0f,0.0f);   // 倾斜视角

       
    现在我们来移动星星。星星开始时位于屏幕的中心。我们要做的第一件事是把场景沿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;   // 若是,使对应的Key数组中的值为 FALSE
       KillGLWindow();    // 销毁当前的窗口
       fullscreen=!fullscreen;   // 切换 全屏 / 窗口 模式
       // 重建 OpenGL 窗口
       if (!CreateGLWindow("NeHe's 透明纹理实例",640,480,16,fullscreen))
       {
        return 0;   // 如果窗口未能创建,程序退出
       }
      }
     }
    }

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


       收藏   分享  
    顶(0)
      




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

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

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给一分之千发送一个短消息 把一分之千加入好友 查看一分之千的个人资料 搜索一分之千在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看一分之千的博客2
    发贴心情 
    Lesson 09
       
    Welcome to Tutorial 9. By now you should have a very good understanding of OpenGL. You've learned everything from setting up an OpenGL Window, to texture mapping a spinning object while using lighting and blending. This will be the first semi-advanced tutorial. You'll learn the following: Moving bitmaps around the screen in 3D, removing the black pixels around the bitmap (using blending), adding color to a black & white texture and finally you'll learn how to create fancy colors and simple animation by mixing different colored textures together.

    We'll be modifying the code from lesson one for this tutorial. We'll start off by adding a few new variables to the beginning of the program. I'll rewrite the entire section of code so it's easier to see where the changes are being made.   
       

    #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

       
    The following lines are new. twinkle and tp are BOOLean variables meaning they can be TRUE or FALSE. twinkle will keep track of whether or not the twinkle effect has been enabled. tp is used to check if the 'T' key has been pressed or released. (pressed tp=TRUE, relased tp=FALSE).   
       

    BOOL twinkle;      // Twinkling Stars
    BOOL tp;       // 'T' Key Pressed?

       
    num will keep track of how many stars we draw to the screen. It's defined as a CONSTant. This means it can never change within the code. The reason we define it as a constant is because you can not redefine an array. So if we've set up an array of only 50 stars and we decided to increase num to 51 somewhere in the code, the array can not grow to 51, so an error would occur. You can change this value to whatever you want it to be in this line only. Don't try to change the value of num later on in the code unless you want disaster to occur.   
       

    const num=50;       // Number Of Stars To Draw

       
    Now we create a structure. The word structure sounds intimidating, but it's not really. A structure is a group simple data (variables, etc) representing a larger similar group. In english :) We know that we're keeping track of stars. You'll see that the 7th line below is stars;. We know each star will have 3 values for color, and all these values will be integer values. The 3rd line int r,g,b sets up 3 integer values. One for red (r), one for green (g), and one for blue (b). We know each star will be a different distance from the center of the screen, and can be place at one of 360 different angles from the center. If you look at the 4th line below, we make a floating point value called dist. This will keep track of the distance. The 5th line creates a floating point value called angle. This will keep track of the stars angle.

    So now we have this group of data that describes the color, distance and angle of a star on the screen. Unfortunately we have more than one star to keep track of. Instead of creating 50 red values, 50 green values, 50 blue values, 50 distance values and 50 angle values, we just create an array called star. Each number in the star array will hold all of the information in our structure called stars. We make the star array in the 8th line below. If we break down the 8th line: stars star[num]. This is what we come up with. The type of array is going to be stars. stars is a structure. So the array is going to hold all of the information in the structure. The name of the array is star. The number of arrays is [num]. So because num=50, we now have an array called star. Our array stores the elements of the structure stars. Alot easier than keeping track of each star with seperate variables. Which would be a very stupid thing to do, and would not allow us to add remove stars by changing the const value of num.   
       

    typedef struct       // Create A Structure For Star
    {
     int r, g, b;      // Stars Color
     GLfloat dist;      // Stars Distance From Center
     GLfloat angle;      // Stars Current Angle
    }
    stars;        // Structures Name Is Stars
    stars star[num];      // Make 'star' Array Of 'num' Using Info From The Structure 'stars'

       
    Next we set up variables to keep track of how far away from the stars the viewer is (zoom), and what angle we're seeing the stars from (tilt). We make a variable called spin that will spin the twinkling stars on the z axis, which makes them look like they are spinning at their current location.

    loop is a variable we'll use in the program to draw all 50 stars, and texture[1] will be used to store the one b&w texture that we load in. If you wanted more textures, you'd increase the value from one to however many textures you decide to use.   
       

    GLfloat zoom=-15.0f;      // Viewing Distance Away From Stars
    GLfloat tilt=90.0f;      // Tilt The View
    GLfloat spin;       // Spin Twinkling Stars

    GLuint loop;       // General Loop Variable
    GLuint texture[1];      // Storage For One Texture

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

       
    Right after the line above we add code to load in our texture. I shouldn't have to explain the code in great detail. It's the same code we used to load the textures in lesson 6, 7 and 8. The bitmap we load this time is called star.bmp. We generate only one texture using glGenTextures(1, &texture[0]). The texture will use linear filtering.   
       

    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 a 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

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

      glGenTextures(1, &texture[0]);   // Create One Texture

      // Create Linear Filtered Texture
      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 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
     }

     return Status;      // Return The Status
    }

       
    Now we set up OpenGL to render the way we want. We're not going to be using Depth Testing in this project, so make sure if you're using the code from lesson one that you remove glDepthFunc(GL_LEQUAL); and glEnable(GL_DEPTH_TEST); otherwise you'll see some very bad results. We're using texture mapping in this code however so you'll want to make sure you add any lines that are not in lesson 1. You'll notice we're enabling texture mapping, along with blending.   
       

    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
     glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Really Nice Perspective Calculations
     glBlendFunc(GL_SRC_ALPHA,GL_ONE);   // Set The Blending Function For Translucency
     glEnable(GL_BLEND);     // Enable Blending

       
    The following code is new. It sets up the starting angle, distance, and color of each star. Notice how easy it is to change the information in the structure. The loop will go through all 50 stars. To change the angle of star[1] all we have to do is say star[1].angle={some number} . It's that simple!   
       

     for (loop=0; loop<num; loop++)    // Create A Loop That Goes Through All The Stars
     {
      star[loop].angle=0.0f;    // Start All The Stars At Angle Zero

       
    I calculate the distance by taking the current star (which is the value of loop) and dividing it by the maximum amount of stars there can be. Then I multiply the result by 5.0f. Basically what this does is moves each star a little bit farther than the previous star. When loop is 50 (the last star), loop divided by num will be 1.0f. The reason I multiply by 5.0f is because 1.0f*5.0f is 5.0f. 5.0f is the very edge of the screen. I don't want stars going off the screen so 5.0f is perfect. If you set the zoom further into the screen you could use a higher number than 5.0f, but your stars would be alot smaller (because of perspective).

    You'll notice that the colors for each star are made up of random values from 0 to 255. You might be wondering how we can use such large values when normally the colors are from 0.0f to 1.0f. When we set the color we'll use glColor4ub instead of glColor4f. ub means Unsigned Byte. A byte can be any value from 0 to 255. In this program it's easier to use bytes than to come up with a random floating point value.   
       

      star[loop].dist=(float(loop)/num)*5.0f;  // Calculate Distance From The Center
      star[loop].r=rand()%256;   // Give star[loop] A Random Red Intensity
      star[loop].g=rand()%256;   // Give star[loop] A Random Green Intensity
      star[loop].b=rand()%256;   // Give star[loop] A Random Blue Intensity
     }
     return TRUE;      // Initialization Went OK
    }

       
    The Resize code is the same, so we'll jump to the drawing code. If you're using the code from lesson one, delete the DrawGLScene code, and just copy what I have below. There's only 2 lines of code in lesson one anyways, so there's not a lot to delete.   
       

    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
     glBindTexture(GL_TEXTURE_2D, texture[0]);  // Select Our Texture

     for (loop=0; loop<num; loop++)    // Loop Through All The Stars
     {
      glLoadIdentity();    // Reset The View Before We Draw Each Star
      glTranslatef(0.0f,0.0f,zoom);   // Zoom Into The Screen (Using The Value In 'zoom')
      glRotatef(tilt,1.0f,0.0f,0.0f);   // Tilt The View (Using The Value In 'tilt')

       
    Now we move the star. The star starts off in the middle of the screen. The first thing we do is spin the scene on the y axis. If we spin 90 degrees, the x axis will no longer run left to right, it will run into and out of the screen. As an example to help clarify. Imagine you were in the center of a room. Now imagine that the left wall had -x written on it, the front wall had -z written on it, the right wall had +x written on it, and the wall behind you had +z written on it. If the room spun 90 degrees to the right, but you did not move, the wall in front of you would no longer say -z it would say -x. All of the walls would have moved. -z would be on the right, +z would be on the left, -x would be in front, and +x would be behind you. Make sense? By rotating the scene, we change the direction of the x and z planes.

    The second line of code moves to a positive value on the x plane. Normally a positive value on x would move us to the right side of the screen (where +x usually is), but because we've rotated on the y plane, the +x could be anywhere. If we rotated by 180 degrees, it would be on the left side of the screen instead of the right. So when we move forward on the positive x plane, we could be moving left, right, forward or backward.   
       

      glRotatef(star[loop].angle,0.0f,1.0f,0.0f); // Rotate To The Current Stars Angle
      glTranslatef(star[loop].dist,0.0f,0.0f); // Move Forward On The X Plane

       
    Now for some tricky code. The star is actually a flat texture. Now if you drew a flat quad in the middle of the screen and texture mapped it, it would look fine. It would be facing you like it should. But if you rotated on the y axis by 90 degrees, the texture would be facing the right and left sides of the screen. All you'd see is a thin line. We don't want that to happen. We want the stars to face the screen all the time, no matter how much we rotate and tilt the screen.

    We do this by cancelling any rotations that we've made, just before we draw the star. You cancel the rotations in reverse order. So above we tilted the screen, then we rotated to the stars current angle. In reverse order, we'd un-rotate (new word) the stars current angle. To do this we use the negative value of the angle, and rotate by that. So if we rotated the star by 10 degrees, rotating it back -10 degrees will make the star face the screen once again on that axis. So the first line below cancels the rotation on the y axis. Then we need to cancel the screen tilt on the x axis. To do that we just tilt the screen by -tilt. After we've cancelled the x and y rotations, the star will face the screen completely.   
       

      glRotatef(-star[loop].angle,0.0f,1.0f,0.0f); // Cancel The Current Stars Angle
      glRotatef(-tilt,1.0f,0.0f,0.0f);  // Cancel The Screen Tilt

       
    If twinkle is TRUE, we'll draw a non-spinning star on the screen. To get a different color, we take the maximum number of stars (num) and subtract the current stars number (loop), then subtract 1 because our loop only goes from 0 to num-1. If the result was 10 we'd use the color from star number 10. That way the color of the two stars is usually different. Not a good way to do it, but effective. The last value is the alpha value. The lower the value, the darker the star is.

    If twinkle is enabled, each star will be drawn twice. This will slow down the program a little depending on what type of computer you have. If twinkle is enabled, the colors from the two stars will mix together creating some really nice colors. Also because this star does not spin, it will appear as if the stars are animated when twinkling is enabled. (look for yourself if you don't understand what I mean).

    Notice how easy it is to add color to the texture. Even though the texture is black and white, it will become whatever color we select before we draw the texture. Also take note that we're using bytes for the color values rather than floating point numbers. Even the alpha value is a byte.   
       

      if (twinkle)     // Twinkling Stars Enabled
      {
       // Assign A Color Using Bytes
       glColor4ub(star[(num-loop)-1].r,star[(num-loop)-1].g,star[(num-loop)-1].b,255);
       glBegin(GL_QUADS);   // Begin Drawing The Textured Quad
        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();    // Done Drawing The Textured Quad
      }

       
    Now we draw the main star. The only difference from the code above is that this star is always drawn, and this star spins on the z axis.   
       

      glRotatef(spin,0.0f,0.0f,1.0f);   // Rotate The Star On The Z Axis
      // Assign A Color Using Bytes
      glColor4ub(star[loop].r,star[loop].g,star[loop].b,255);
      glBegin(GL_QUADS);    // Begin Drawing The Textured Quad
       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();     // Done Drawing The Textured Quad

       
    Here's where we do all the movement. We spin the normal stars by increasing the value of spin. Then we change the angle of each star. The angle of each star is increased by loop/num. What this does is spins the stars that are farther from the center faster. The stars closer to the center spin slower. Finally we decrease the distance each star is from the center of the screen. This makes the stars look as if they are being sucked into the middle of the screen.   
       

      spin+=0.01f;     // Used To Spin The Stars
      star[loop].angle+=float(loop)/num;  // Changes The Angle Of A Star
      star[loop].dist-=0.01f;    // Changes The Distance Of A Star

       
    The lines below check to see if the stars have hit the center of the screen or not. When a star hits the center of the screen it's given a new color, and is moved 5 units from the center, so it can start it's journey back to the center as a new star.   
       

      if (star[loop].dist<0.0f)   // Is The Star In The Middle Yet
      {
       star[loop].dist+=5.0f;   // Move The Star 5 Units From The Center
       star[loop].r=rand()%256;  // Give It A New Red Value
       star[loop].g=rand()%256;  // Give It A New Green Value
       star[loop].b=rand()%256;  // Give It A New Blue Value
      }
     }
     return TRUE;      // Everything Went OK
    }

       
    Now we're going to add code to check if any keys are being pressed. Go down to WinMain(). Look for the line SwapBuffers(hDC). We'll add our key checking code right under that line. lines of code.

    The lines below check to see if the T key has been pressed. If it has been pressed and it's not being held down the following will happen. If twinkle is FALSE, it will become TRUE. If it was TRUE, it will become FALSE. Once T is pressed tp will become TRUE. This prevents the code from running over and over again if you hold down the T key.   
       

      SwapBuffers(hDC);    // Swap Buffers (Double Buffering)
      if (keys['T'] && !tp)    // Is T Being Pressed And Is tp FALSE
      {
       tp=TRUE;    // If So, Make tp TRUE
       twinkle=!twinkle;   // Make twinkle Equal The Opposite Of What It Is
      }

       
    The code below checks to see if you've let go of the T key. If you have, it makes tp=FALSE. Pressing the T key will do nothing unless tp is FALSE, so this section of code is very important.   
       

      if (!keys['T'])     // Has The T Key Been Released
      {
       tp=FALSE;    // If So, make tp FALSE
      }

       
    The rest of the code checks to see if the up arrow, down arrow, page up or page down keys are being pressed.   
       

      if (keys[VK_UP])    // Is Up Arrow Being Pressed
      {
       tilt-=0.5f;    // Tilt The Screen Up
      }

      if (keys[VK_DOWN])    // Is Down Arrow Being Pressed
      {
       tilt+=0.5f;    // Tilt The Screen Down
      }

      if (keys[VK_PRIOR])    // Is Page Up Being Pressed
      {
       zoom-=0.2f;    // Zoom Out
      }

      if (keys[VK_NEXT])    // Is Page Down Being Pressed
      {
       zoom+=0.2f;    // Zoom In
      }

       
    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
       }
      }
     }
    }

       
    In this tutorial I have tried to explain in as much detail how to load in a gray scale bitmap image, remove the black space around the image (using blending), add color to the image, and move the image around the screen in 3D. I've also shown you how to create beautiful colors and animation by overlapping a second copy of the bitmap on top of the original bitmap. Once you have a good understanding of everything I've taught you up till now, you should have no problems making 3D demos ofyour own. All the basics have been covered!

    Jeff Molofee (NeHe)

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

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

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

    第十课    


    按此在新窗口浏览图片加载3D世界,并在其中漫游:

    在这一课中,你将学会如何加载3D世界,并在3D世界中漫游。这一课使用第一课的代码,当然在课程说明中我只介绍改变了代码。

      
       
       
    这一课是由Lionel Brits (βtelgeuse)所写的。在本课中我们只对增加的代码做解释。当然只添加课程中所写的代码,程序是不会运行的。如果您有兴趣知道下面的每一行代码是如何运行的话,请下载完整的源码,并在浏览这一课的同时,对源码进行跟踪。
    好了现在欢迎来到名不见经传的第十课。到现在为止,您应该有能力创建一个旋转的立方体或一群星星了,对3D编程也应该有些感觉了吧?但还是请等一下!不要立马冲动地要开始写个Quake
    IV,好不好...:)。只靠旋转的立方体还很难来创造一个可以决一死战的酷毙了的对手....:)。现在这些日子您所需要的是一个大一点的、更复杂些的、动态3D世界,它带有空间的六自由度和花哨的效果如镜像、入口、扭曲等等,当然还要有更快的帧显示速度。这一课就要解释一个基本的3D世界"结构",以及如何在这个世界里游走。
    数据结构
    当您想要使用一系列的数字来完美的表达3D环境时,随着环境复杂度的上升,这个工作的难度也会随之上升。出于这个原因,我们必须将数据归类,使其具有更多的可操作性风格。在程序清单头部出现了sector(区段)的定义。每个3D世界基本上可以看作是sector(区段)的集合。一个sector(区段)可以是一个房间、一个立方体、或者任意一个闭合的区间。  
       

    typedef struct tagSECTOR      // 创建Sector区段结构
    {
     int numtriangles;      // Sector中的三角形个数
     TRIANGLE* triangle;      // 指向三角数组的指针
    } SECTOR;        // 命名为SECTOR

       
    一个sector(区段)包含了一系列的多边形,所以下一个目标就是triangle(我们将只用三角形,这样写代码更容易些)。  
       

    typedef struct tagTRIANGLE      // 创建Triangle三角形结构
    {
     VERTEX vertex[3];      // VERTEX矢量数组,大小为3
    } TRIANGLE;        // 命名为 TRIANGLE

       
    三角形本质上是由一些(两个以上)顶点组成的多边形,顶点同时也是我们的最基本的分类单位。顶点包含了OpenGL真正感兴趣的数据。我们用3D空间中的坐标值(x,y,z)以及它们的纹理坐标(u,v)来定义三角形的每个顶点。  
       

    typedef struct tagVERTEX      // 创建Vertex顶点结构
    {
     float x, y, z;       // 3D 坐标
     float u, v;       // 纹理坐标
    } VERTEX;        // 命名为VERTEX

       
    载入文件
    在程序内部直接存储数据会让程序显得太过死板和无趣。从磁盘上载入世界资料,会给我们带来更多的弹性,可以让我们体验不同的世界,而不用被迫重新编译程序。另一个好处就是用户可以切换世界资料并修改它们而无需知道程序如何读入输出这些资料的。数据文件的类型我们准备使用文本格式。这样编辑起来更容易,写的代码也更少。等将来我们也许会使用二进制文件。

    问题是,怎样才能从文件中取得数据资料呢?首先,创建一个叫做SetupWorld()的新函数。把这个文件定义为filein,并且使用只读方式打开文件。我们必须在使用完毕之后关闭文件。大家一起来看看现在的代码:

      
       

    // 先前的定义: char* worldfile = "data\\world.txt";
    void SetupWorld()       // 设置我们的世界
    {
     FILE *filein;       // 工作文件
     filein = fopen(worldfile, "rt");    // 打开文件

     ...
     (读入数据资料))
     ...

     fclose(filein);       // 关闭文件
     return;        // 返回
    }

       
    下一个挑战是将每个单独的文本行读入变量。这有很多办法可以做到。一个问题是文件中并不是所有的行都包含有意义的信息。空行和注释不应该被读入。我们创建了一个叫做readstr()的函数。这个函数会从数据文件中读入一个有意义的行至一个已经初始化过的字符串。下面就是代码:
      
       

    void readstr(FILE *f,char *string)     //  读入一个字符串

    {
     do        // 循环开始
     {
      fgets(string, 255, f);     // 读入一行
     } while ((string[0] == '/') || (string[0] == '\n'));  // 考察是否有必要进行处理
     return;        // 返回
    }

       
    下一步我们读入区段数据。这一课将只处理一个区段,不过实现一个多区段引擎也很容易。让我们将注意力转回SetupWorld()。程序必须知道区段内包含了多少个三角形。我们在数据文件中以下面这种形式定义三角形数量:
    接下来是读取三角形数量的代码:  
       

    int numtriangles;       // 区段中的三角形数量
    char oneline[255];       // 存储数据的字符串
    ...
    readstr(filein,oneline);      // 读入一行数据
    sscanf(oneline, "NUMPOLLIES %d\n", &numtriangles);   // 读入三角形数量

       
    余下的世界载入过程采用了相似的方法。接着,我们对区段进行初始化,并读入部分数据:  
       

    // 先前的定义: SECTOR sector1;
    char oneline[255];       // 存储数据的字符串
    int numtriangles;       // 区段的三角形数量
    float x, y, z, u, v;       // 3D 和 纹理坐标
    ...
    sector1.triangle = new TRIANGLE[numtriangles];    // 为numtriangles个三角形分配内存并设定指针
    sector1.numtriangles = numtriangles;     // 定义区段1中的三角形数量
    // 遍历区段中的每个三角形
    for (int triloop = 0; triloop < numtriangles; triloop++)  // 遍历所有的三角形
    {
     // 遍历三角形的每个顶点
     for (int vertloop = 0; vertloop < 3; vertloop++)  // 遍历所有的顶点
     {
      readstr(filein,oneline);    // 读入一行数据
      // 读入各自的顶点数据
      sscanf(oneline, "%f %f %f %f %f", &x, &y, &z, &u, &v);
      // 将顶点数据存入各自的顶点
      sector1.triangle[triloop].vertex[vertloop].x = x; // 区段 1,  第 triloop 个三角形, 第  vertloop 个顶点, 值 x =x
      sector1.triangle[triloop].vertex[vertloop].y = y; // 区段 1,  第 triloop 个三角形, 第  vertloop 个顶点, 值 y =y
      sector1.triangle[triloop].vertex[vertloop].z = z; // 区段 1,  第 triloop 个三角形, 第  vertloop 个顶点, 值  z =z
      sector1.triangle[triloop].vertex[vertloop].u = u; // 区段 1,  第 triloop 个三角形, 第  vertloop 个顶点, 值  u =u
      sector1.triangle[triloop].vertex[vertloop].v = v; // 区段 1,  第 triloop 个三角形, 第  vertloop 个顶点, 值  e=v
     }
    }

       
      数据文件中每个三角形都以如下形式声明:
    X1 Y1 Z1 U1 V1
    X2 Y2 Z2 U2 V2
    X3 Y3 Z3 U3 V3
    显示世界
    现在区段已经载入内存,我们下一步要在屏幕上显示它。到目前为止,我们所作过的都是些简单的旋转和平移。但我们的镜头始终位于原点(0,0,0)处。任何一个不错的3D引擎都会允许用户在这个世界中游走和遍历,我们的这个也一样。实现这个功能的一种途径是直接移动镜头并绘制以镜头为中心的3D环境。这样做会很慢并且不易用代码实现。我们的解决方法如下:
    根据用户的指令旋转并变换镜头位置。
    围绕原点,以与镜头相反的旋转方向来旋转世界。(让人产生镜头旋转的错觉)
    以与镜头平移方式相反的方式来平移世界(让人产生镜头移动的错觉)。
    这样实现起来就很简单.
    下面从第一步开始吧(平移并旋转镜头)。

      
       

    if (keys[VK_RIGHT])       // 右方向键按下了么?
    {
     yrot -= 1.5f;       // 向左旋转场景
    }

    if (keys[VK_LEFT])       // 左方向键按下了么?
    {
     yrot += 1.5f;       // 向右侧旋转场景
    }

    if (keys[VK_UP])       // 向上方向键按下了么?
    {
     xpos -= (float)sin(heading*piover180) * 0.05f;   // 沿游戏者所在的X平面移动
     zpos -= (float)cos(heading*piover180) * 0.05f;   // 沿游戏者所在的Z平面移动
     if (walkbiasangle >= 359.0f)     // 如果walkbiasangle大于359度
     {
      walkbiasangle = 0.0f;     // 将 walkbiasangle 设为0
     }
     else        // 否则
     {
       walkbiasangle+= 10;     // 如果 walkbiasangle < 359 ,则增加 10
     }
     walkbias = (float)sin(walkbiasangle * piover180)/20.0f;  // 使游戏者产生跳跃感
    }

    if (keys[VK_DOWN])       // 向下方向键按下了么?
    {
     xpos += (float)sin(heading*piover180) * 0.05f;   // 沿游戏者所在的X平面移动
     zpos += (float)cos(heading*piover180) * 0.05f;   // 沿游戏者所在的Z平面移动
     if (walkbiasangle <= 1.0f)     // 如果walkbiasangle小于1度
     {
      walkbiasangle = 359.0f;     // 使 walkbiasangle 等于 359
     }
     else        // 否则
     {
      walkbiasangle-= 10;     // 如果 walkbiasangle > 1 减去 10
     }
     walkbias = (float)sin(walkbiasangle * piover180)/20.0f;  // 使游戏者产生跳跃感
    }

       
    这个实现很简单。当左右方向键按下后,旋转变量yrot
    相应增加或减少。当前后方向键按下后,我们使用sine和cosine函数重新生成镜头位置(您需要些许三角函数学的知识:-)。Piover180
    是一个很简单的折算因子用来折算度和弧度。
    接着您可能会问:walkbias是什么意思?这是NeHe的发明的单词:-)。基本上就是当人行走时头部产生上下摆动的幅度。我们使用简单的sine正弦波来调节镜头的Y轴位置。如果不添加这个而只是前后移动的话,程序看起来就没这么棒了。
    现在,我们已经有了下面这些变量。可以开始进行步骤2和3了。由于我们的程序还不太复杂,我们无需新建一个函数,而是直接在显示循环中完成这些步骤。  
       

    int DrawGLScene(GLvoid)       // 绘制 OpenGL 场景
    {
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  // 清除 场景 和 深度缓冲
     glLoadIdentity();      // 重置当前矩阵


     GLfloat x_m, y_m, z_m, u_m, v_m;    // 顶点的临时 X, Y, Z, U 和 V 的数值
     GLfloat xtrans = -xpos;      // 用于游戏者沿X轴平移时的大小
     GLfloat ztrans = -zpos;      // 用于游戏者沿Z轴平移时的大小
     GLfloat ytrans = -walkbias-0.25f;    // 用于头部的上下摆动
     GLfloat sceneroty = 360.0f - yrot;    // 位于游戏者方向的360度角

     int numtriangles;      // 保有三角形数量的整数

     glRotatef(lookupdown,1.0f,0,0);     // 上下旋转
     glRotatef(sceneroty,0,1.0f,0);     // 根据游戏者正面所对方向所作的旋转

     glTranslatef(xtrans, ytrans, ztrans);    // 以游戏者为中心的平移场景
     glBindTexture(GL_TEXTURE_2D, texture[filter]);   // 根据 filter 选择的纹理

     numtriangles = sector1.numtriangles;    // 取得Sector1的三角形数量

     // 逐个处理三角形
     for (int loop_m = 0; loop_m < numtriangles; loop_m++)  // 遍历所有的三角形
     {
      glBegin(GL_TRIANGLES);     // 开始绘制三角形
       glNormal3f( 0.0f, 0.0f, 1.0f);   // 指向前面的法线
       x_m = sector1.triangle[loop_m].vertex[0].x; // 第一点的 X 分量
       y_m = sector1.triangle[loop_m].vertex[0].y; // 第一点的 Y 分量
       z_m = sector1.triangle[loop_m].vertex[0].z; // 第一点的 Z 分量
       u_m = sector1.triangle[loop_m].vertex[0].u; // 第一点的 U  纹理坐标
       v_m = sector1.triangle[loop_m].vertex[0].v; // 第一点的 V  纹理坐标
       glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m); // 设置纹理坐标和顶点

       x_m = sector1.triangle[loop_m].vertex[1].x; // 第二点的 X 分量
       y_m = sector1.triangle[loop_m].vertex[1].y; // 第二点的 Y 分量
       z_m = sector1.triangle[loop_m].vertex[1].z; // 第二点的 Z 分量
       u_m = sector1.triangle[loop_m].vertex[1].u; // 第二点的 U  纹理坐标
       v_m = sector1.triangle[loop_m].vertex[1].v; // 第二点的 V  纹理坐标
       glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m); // 设置纹理坐标和顶点

       x_m = sector1.triangle[loop_m].vertex[2].x; // 第三点的 X 分量
       y_m = sector1.triangle[loop_m].vertex[2].y; // 第三点的 Y 分量
       z_m = sector1.triangle[loop_m].vertex[2].z; // 第三点的 Z 分量
       u_m = sector1.triangle[loop_m].vertex[2].u; // 第二点的 U  纹理坐标
       v_m = sector1.triangle[loop_m].vertex[2].v; // 第二点的 V  纹理坐标
       glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m); // 设置纹理坐标和顶点
      glEnd();      // 三角形绘制结束
     }
     return TRUE;       // 返回
    }

       
    搞定!我们已经完成了自己的第一帧画面。这绝对算不上什么Quake,但咳...,我们绝对也不是Carmack或者Abrash。运行程序时,您可以按下F、B、
    PgUp 和 PgDown 键来看看效果。PgUp /
    PgDown简单的上下倾斜镜头。如果NeHe决定保留的话,程序中使用的纹理取自于我的学校ID证件上的照片,并且做了浮雕效果....:)。
    现在您也许在考虑下一步该做什么。但还是不要考虑使用这些代码来实现完整的3D引擎,写这个程序的目的也并非如此。您也许希望您的游戏中不止存在一个Sector,尤其是实现类似入口这样的部分,您还可能需要使用多边形(超过3个顶点)。程序现在的代码实现允许载入多个Sector并剔除了背面(背向镜头不用绘制的多边形)。将来我会写个这样的教程,但这需要更多的数学知识基础。

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

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

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给一分之千发送一个短消息 把一分之千加入好友 查看一分之千的个人资料 搜索一分之千在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看一分之千的博客4
    发贴心情 
    Lesson 10
       
    This tutorial was created by Lionel Brits (遝telgeuse). This lesson only explains the sections of code that have been added. By adding just the lines below, the program will not run. If you're interested to know where each of the lines of code below go, download the source code, and follow through it, as you read the tutorial.

    Welcome to the infamous Tutorial 10. By now you have a spinning cube or a couple of stars, and you have the basic feel for 3D programming. But wait! Don't run off and start to code Quake IV just yet. Spinning cubes just aren't going to make cool deathmatch opponents :-) These days you need a large, complicated and dynamic 3D world with 6 degrees of freedom and fancy effects like mirrors, portals, warping and of course, high framerates. This tutorial explains the basic "structure" of a 3D world, and also how to move around in it.

    Data structure

    While it is perfectly alright to code a 3D environment as a long series of numbers, it becomes increasingly hard as the complexity of the environment goes up. For this reason, we must catagorize our data into a more workable fashion. At the top of our list is the sector. Each 3D world is basically a collection of sectors. A sector can be a room, a cube, or any enclosed volume.   
       

    typedef struct tagSECTOR      // Build Our Sector Structure
    {
     int numtriangles;      // Number Of Triangles In Sector
     TRIANGLE* triangle;      // Pointer To Array Of Triangles
    } SECTOR;        // Call It SECTOR

       
    A sector holds a series of polygons, so the next catagory will be the triangle (we will stick to triangles for now, as they are alot easier to code.)   
       

    typedef struct tagTRIANGLE      // Build Our Triangle Structure
    {
     VERTEX vertex[3];      // Array Of Three Vertices
    } TRIANGLE;        // Call It TRIANGLE

       
    The triangle is basically a polygon made up of vertices (plural of vertex), which brings us to our last catagory. The vertex holds the real data that OpenGL is interested in. We define each point on the triangle with it's position in 3D space (x, y, z) as well as it's texture coordinates (u, v).   
       

    typedef struct tagVERTEX      // Build Our Vertex Structure
    {
     float x, y, z;       // 3D Coordinates
     float u, v;       // Texture Coordinates
    } VERTEX;        // Call It VERTEX

       
    Loading files

    Storing our world data inside our program makes our program quite static and boring. Loading worlds from disk, however, gives us much more flexibility as we can test different worlds without having to recompile our program. Another advantage is that the user can interchange worlds and modify them without having to know the in's and out's of our program. The type of data file we are going to be using will be text. This makes for easy editing, and less code. We will leave binary files for a later date.

    The question is, how do we get our data from our file. First, we create a new function called SetupWorld(). We define our file as filein, and we open it for read-only access. We must also close our file when we are done. Let us take a look at the code so far:   
       

    // Previous Declaration: char* worldfile = "data\\world.txt";
    void SetupWorld()       // Setup Our World
    {
     FILE *filein;       // File To Work With
     filein = fopen(worldfile, "rt");    // Open Our File

     ...
     (read our data)
     ...

     fclose(filein);       // Close Our File
     return;        // Jump Back
    }

       
    Our next challenge is to read each individual line of text into a variable. This can be done in a number of ways. One problem is that not all lines in the file will contain meaningful information. Blank lines and comments shouldn't be read. Let us create a function called readstr(). This function will read one meaningful line of text into an initialised string. Here's the code:   
       

    void readstr(FILE *f,char *string)     // Read In A String

    {
     do        // Start A Loop
     {
      fgets(string, 255, f);     // Read One Line
     } while ((string[0] == '/') || (string[0] == '\n'));  // See If It Is Worthy Of Processing
     return;        // Jump Back
    }

       
    Next, we must read in the sector data. This lesson will deal with one sector only, but it is easy to implement a multi-sector engine. Let us turn back to SetupWorld().Our program must know how many triangles are in our sector. In our data file, we will define the number of triangles as follows:

    NUMPOLLIES n

    Here's the code to read the number of triangles:   
       

    int numtriangles;       // Number Of Triangles In Sector
    char oneline[255];       // String To Store Data In
    ...
    readstr(filein,oneline);      // Get Single Line Of Data
    sscanf(oneline, "NUMPOLLIES %d\n", &numtriangles);   // Read In Number Of Triangles

       
    The rest of our world-loading process will use the same process. Next, we initialize our sector and read some data into it:   
       

    // Previous Declaration: SECTOR sector1;
    char oneline[255];       // String To Store Data In
    int numtriangles;       // Number Of Triangles In Sector
    float x, y, z, u, v;       // 3D And Texture Coordinates
    ...
    sector1.triangle = new TRIANGLE[numtriangles];    // Allocate Memory For numtriangles And Set Pointer
    sector1.numtriangles = numtriangles;     // Define The Number Of Triangles In Sector 1
    // Step Through Each Triangle In Sector
    for (int triloop = 0; triloop < numtriangles; triloop++)  // Loop Through All The Triangles
    {
     // Step Through Each Vertex In Triangle
     for (int vertloop = 0; vertloop < 3; vertloop++)  // Loop Through All The Vertices
     {
      readstr(filein,oneline);    // Read String To Work With
      // Read Data Into Respective Vertex Values
      sscanf(oneline, "%f %f %f %f %f", &x, &y, &z, &u, &v);
      // Store Values Into Respective Vertices
      sector1.triangle[triloop].vertex[vertloop].x = x; // Sector 1, Triangle triloop, Vertice vertloop, x Value=x
      sector1.triangle[triloop].vertex[vertloop].y = y; // Sector 1, Triangle triloop, Vertice vertloop, y Value=y
      sector1.triangle[triloop].vertex[vertloop].z = z; // Sector 1, Triangle triloop, Vertice vertloop, z Value=z
      sector1.triangle[triloop].vertex[vertloop].u = u; // Sector 1, Triangle triloop, Vertice vertloop, u Value=u
      sector1.triangle[triloop].vertex[vertloop].v = v; // Sector 1, Triangle triloop, Vertice vertloop, v Value=v
     }
    }

       
    Each triangle in our data file is declared as follows:
    X1 Y1 Z1 U1 V1
    X2 Y2 Z2 U2 V2
    X3 Y3 Z3 U3 V3
    Displaying Worlds

    Now that we can load        our sector into memory, we need to display it on screen. So far we have        done some minor rotations and translations, but our camera was always        centered at the origin (0,0,0). Any good 3D engine would have the user be        able to walk around and explore the world, and so will ours. One way of        doing this is to move the camera around and draw the 3D environment        relative to the camera position. This is slow and hard to code. What we        will do is this:        
    Rotate and translate the camera position according to user commands
    Rotate the world around the origin in the opposite direction of the camera rotation (giving the illusion that the camera has been rotated)
    Translate the world in the opposite manner that the camera has been translated (again, giving the illusion that the camera has moved)
    This is pretty simple to implement. Let's start with the first stage (Rotation and translation of the camera).
      
       

    if (keys[VK_RIGHT])       // Is The Right Arrow Being Pressed?
    {
     yrot -= 1.5f;       // Rotate The Scene To The Left
    }

    if (keys[VK_LEFT])       // Is The Left Arrow Being Pressed?
    {
     yrot += 1.5f;       // Rotate The Scene To The Right 
    }

    if (keys[VK_UP])       // Is The Up Arrow Being Pressed?
    {
     xpos -= (float)sin(heading*piover180) * 0.05f;   // Move On The X-Plane Based On Player Direction
     zpos -= (float)cos(heading*piover180) * 0.05f;   // Move On The Z-Plane Based On Player Direction
     if (walkbiasangle >= 359.0f)     // Is walkbiasangle>=359?
     {
      walkbiasangle = 0.0f;     // Make walkbiasangle Equal 0
     }
     else        // Otherwise
     {
       walkbiasangle+= 10;     // If walkbiasangle < 359 Increase It By 10
     }
     walkbias = (float)sin(walkbiasangle * piover180)/20.0f;  // Causes The Player To Bounce
    }

    if (keys[VK_DOWN])       // Is The Down Arrow Being Pressed?
    {
     xpos += (float)sin(heading*piover180) * 0.05f;   // Move On The X-Plane Based On Player Direction
     zpos += (float)cos(heading*piover180) * 0.05f;   // Move On The Z-Plane Based On Player Direction
     if (walkbiasangle <= 1.0f)     // Is walkbiasangle<=1?
     {
      walkbiasangle = 359.0f;     // Make walkbiasangle Equal 359
     }
     else        // Otherwise
     {
      walkbiasangle-= 10;     // If walkbiasangle > 1 Decrease It By 10
     }
     walkbias = (float)sin(walkbiasangle * piover180)/20.0f;  // Causes The Player To Bounce
    }

       
    That was fairly simple. When either the left or right cursor key is pressed, the rotation variable yrot is incremented or decremented appropriatly. When the forward or backwards cursor key is pressed, a new location for the camera is calculated using the sine and cosine calculations (some trigonometry required :-). Piover180 is simply a conversion factor for converting between degrees and radians.

    Next you ask me: What is this walkbias? It's a word I invented :-) It's basically an offset that occurs when a person walks around (head bobbing up and down like a buoy. It simply adjusts the camera's Y position with a sine wave. I had to put this in, as simply moving forwards and backwards didn't look to great.

    Now that we have these variables down, we can proceed with steps two and three. This will be done in the display loop, as our program isn't complicated enough to merit a seperate function.   
       

    int DrawGLScene(GLvoid)       // Draw The OpenGL Scene
    {
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  // Clear Screen And Depth Buffer
     glLoadIdentity();      // Reset The Current Matrix


     GLfloat x_m, y_m, z_m, u_m, v_m;    // Floating Point For Temp X, Y, Z, U And V Vertices
     GLfloat xtrans = -xpos;      // Used For Player Translation On The X Axis
     GLfloat ztrans = -zpos;      // Used For Player Translation On The Z Axis
     GLfloat ytrans = -walkbias-0.25f;    // Used For Bouncing Motion Up And Down
     GLfloat sceneroty = 360.0f - yrot;    // 360 Degree Angle For Player Direction

     int numtriangles;      // Integer To Hold The Number Of Triangles

     glRotatef(lookupdown,1.0f,0,0);     // Rotate Up And Down To Look Up And Down
     glRotatef(sceneroty,0,1.0f,0);     // Rotate Depending On Direction Player Is Facing

     glTranslatef(xtrans, ytrans, ztrans);    // Translate The Scene Based On Player Position
     glBindTexture(GL_TEXTURE_2D, texture[filter]);   // Select A Texture Based On filter

     numtriangles = sector1.numtriangles;    // Get The Number Of Triangles In Sector 1

     // Process Each Triangle
     for (int loop_m = 0; loop_m < numtriangles; loop_m++)  // Loop Through All The Triangles
     {
      glBegin(GL_TRIANGLES);     // Start Drawing Triangles
       glNormal3f( 0.0f, 0.0f, 1.0f);   // Normal Pointing Forward
       x_m = sector1.triangle[loop_m].vertex[0].x; // X Vertex Of 1st Point
       y_m = sector1.triangle[loop_m].vertex[0].y; // Y Vertex Of 1st Point
       z_m = sector1.triangle[loop_m].vertex[0].z; // Z Vertex Of 1st Point
       u_m = sector1.triangle[loop_m].vertex[0].u; // U Texture Coord Of 1st Point
       v_m = sector1.triangle[loop_m].vertex[0].v; // V Texture Coord Of 1st Point
       glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m); // Set The TexCoord And Vertice

       x_m = sector1.triangle[loop_m].vertex[1].x; // X Vertex Of 2nd Point
       y_m = sector1.triangle[loop_m].vertex[1].y; // Y Vertex Of 2nd Point
       z_m = sector1.triangle[loop_m].vertex[1].z; // Z Vertex Of 2nd Point
       u_m = sector1.triangle[loop_m].vertex[1].u; // U Texture Coord Of 2nd Point
       v_m = sector1.triangle[loop_m].vertex[1].v; // V Texture Coord Of 2nd Point
       glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m); // Set The TexCoord And Vertice

       x_m = sector1.triangle[loop_m].vertex[2].x; // X Vertex Of 3rd Point
       y_m = sector1.triangle[loop_m].vertex[2].y; // Y Vertex Of 3rd Point
       z_m = sector1.triangle[loop_m].vertex[2].z; // Z Vertex Of 3rd Point
       u_m = sector1.triangle[loop_m].vertex[2].u; // U Texture Coord Of 3rd Point
       v_m = sector1.triangle[loop_m].vertex[2].v; // V Texture Coord Of 3rd Point
       glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m); // Set The TexCoord And Vertice
      glEnd();      // Done Drawing Triangles
     }
     return TRUE;       // Jump Back
    }

       
    And voila! We have drawn our first frame. This isn't exactly Quake but hey, we aren't exactly Carmack's or Abrash's. While running the program, you may want to press F, B, PgUp and PgDown to see added effects. PgUp/Down simply tilts the camera up and down (the same process as panning from side to side.) The texture included is simply a mud texture with a bumpmap of my school ID picture; that is, if NeHe decided to keep it :-).

    So now you're probably thinking where to go next. Don't even consider using this code to make a full-blown 3D engine, since that's not what it's designed for. You'll probably want more than one sector in your game, especially if you're going to implement portals. You'll also want to have polygons with more than 3 vertices, again, essential for portal engines. My current implementation of this code allows for multiple sector loading and does backface culling (not drawing polygons that face away from the camera). I'll write a tutorial on that soon, but as it uses alot of math, I'm going to write a tutorial on matrices first.

    NeHe (05/01/00):

    I've added FULL comments to each of the lines listed in this tutorial. Hopefully things make more sense now. Only a few of the lines had comments after them, now they all do :)

    Please, if you have any problems with the code/tutorial (this is my first tutorial, so my explanations are a little vague), don't hesitate to email me mailto:iam@cadvision.com Until next time...

    Lionel Brits (遝telgeuse)

    Jeff Molofee (NeHe)

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

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/10/15 16:52:00
     
     长风万里 美女呀,离线,快来找我吧!
      
      
      等级:大一新生
      文章:3
      积分:77
      门派:W3CHINA.ORG
      注册:2007/10/28

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给长风万里发送一个短消息 把长风万里加入好友 查看长风万里的个人资料 搜索长风万里在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看长风万里的博客5
    发贴心情 
    谢谢你,正在学习OpenGL,不知道我能下载附件吗?
    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2007/10/28 4:58:00
     
     cjopengler 帅哥哟,离线,有人找我吗?
      
      
      等级:大一新生
      文章:1
      积分:56
      门派:XML.ORG.CN
      注册:2009/11/30

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给cjopengler发送一个短消息 把cjopengler加入好友 查看cjopengler的个人资料 搜索cjopengler在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看cjopengler的博客6
    发贴心情 
    第9课翻译有错误:
    spin+=0.01f;     // 星星的公转   ===>> 星星的自转
    star[loop].angle+=float(loop)/num;  // 改变星星的自转角度 ===>>星星的公转角度
    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2009/12/8 10:02:00
     
     JL2012JL 帅哥哟,离线,有人找我吗?
      
      
      等级:大一新生
      文章:1
      积分:54
      门派:XML.ORG.CN
      注册:2013/4/11

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给JL2012JL发送一个短消息 把JL2012JL加入好友 查看JL2012JL的个人资料 搜索JL2012JL在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看JL2012JL的博客7
    发贴心情 
    这教程真心不错,学习了
    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2013/4/11 13:29:00
     
     GoogleAdSense
      
      
      等级:大一新生
      文章:1
      积分:50
      门派:无门无派
      院校:未填写
      注册:2007-01-01
    给Google AdSense发送一个短消息 把Google AdSense加入好友 查看Google AdSense的个人资料 搜索Google AdSense在『 C/C++编程思想 』的所有贴子 访问Google AdSense的主页 引用回复这个贴子 回复这个贴子 查看Google AdSense的博客广告
    2024/10/6 13:31:45

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

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