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

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

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

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


    第21 22课源码下载


    按此在新窗口浏览图片线,反走样,计时,正投影和简单的声音:

    这是我第一个大的教程,它将包括线,反走样,计时,正投影和简单的声音。希望这一课中的东西能让每个人感到高兴。

      
       
       
    欢迎来到第21课,在这一课里,你将学会直线,反走样,正投影,计时,基本的音效和一个简单的游戏逻辑。希望这里的东西可以让你高兴。我花了两天的时间写代码,并用了两周的时间写这份HTML文件,希望你能享受我的劳动。
    在这课的结尾你将获得一个叫"amidar"的游戏,你的任务是走完所有的直线。这个程序有了一个基本游戏的一切要素,关卡,生命值,声音和一个游戏道具。

    我们从第一课的程序来逐步完整这个程序,按照惯例,我们只介绍改动的部分。
      
       

    #include <windows.h>       
    #include <stdio.h>       
    #include <stdarg.h>      
    #include <gl\gl.h>       
    #include <gl\glu.h>       
    #include <gl\glaux.h>       

    HDC  hDC=NULL;       
    HGLRC  hRC=NULL;       
    HWND  hWnd=NULL;       
    HINSTANCE hInstance;       
       
    bool类型的变量,vline保存了组成我们游戏网格垂直方向上的121条线,上下水平各11条。hline保存了水平方向上的 121条线,用ap来检查A键是否已经按下。
    当网格被填满时, filled被设置为TRUE而反之则为FALSE。gameover这个变量的作用显而易见,当他的值为TRUE时,游戏结束。anti指出抗锯齿功能是否打开,当设置为TRUE时,该功能是打开着的。active 和 fullscreen 指出窗口是否被最小化以及游戏窗口是窗口模式还是全屏模式。
      
       

    bool  keys[256];       
    bool  vline[11][10];       // 保存垂直方向的11根线条中,每根线条中的10段是否被走过
    bool  hline[10][11];       //保存水平方向的11根线条中,每根线条中的10段是否被走过
    bool  ap;        // A键是否已经按下
    bool  filled;        // 网格是否被填满?
    bool  gameover;       // 游戏是否结束?
    bool  anti=TRUE;       // 是否启用反走样?
    bool  active=TRUE;       
    bool  fullscreen=TRUE;      
       
    接着设置整型变量。loop1 和 loop2 被用来检查网格,查看是否有敌人攻击我们,以及在网格上给对象一个随机的位置。你将看到loop1 / loop2在后面的程序得到使用。delay 是一个计数器,我用他来减慢那些坏蛋的动作。当delay的值大于某一个馈值的时候,敌人才可以行动,此时delay将被重置。
    adjust是一个非常特殊的变量,即使我们的程序拥有一个定时器,他也仅仅用来检查你的计算机是否运行地太快。如果是,则需要暂停一下以减慢运行速度。在我地GeForce显卡上,程序的运行平滑地简直变态,并且非常非常快。但是在我的PIII/450+Voodoo 3500TV上测试的时候,我注意到程序运行地非常缓慢。我发现问题在于关于时间控制那部分代码只能够用来减慢游戏进行而并不能加速之。因此我引入了一个叫做adjust 的变量。它可以是0到5之间的任何值。游戏中的对象移动速度的不同依赖于这个变量的值。值越小,运动越平滑;而值越大,则运动速度越快。这是在比较慢的机器上运行这个程序最简单有效的解决方案了。但是请注意,不管对象移动的速度有多快,游戏的速度都不会比我期望的更快。我们推荐把adjust值设置为3,这样在大部分机器上都有比较满意的效果。
    我们把lives的值设置成5,这样我们的英雄一出场就拥有5条命。level是一个内部变量,用来指出当前游戏的难度。当然,这并不是你在屏幕上所看到的那个Level。变量level2开始的时候和Level拥有相同的值,但是随着你技能的提高,这个值也会增加。当你成功通过难度3之后,这个值也将在难度3上停止增加。level 是一个用来表示游戏难度的内部变量,stage才是用来记录当前游戏关卡的变量。  
       

    int  loop1;        // 通用循环变量
    int  loop2;        // 通用循环变量
    int  delay;        // 敌人的暂停时间
    int  adjust=3;       // 调整显示的速度
    int  lives=5;       // 玩家的生命
    int  level=1;       // 内部游戏的等级
    int  level2=level;       // 显示的游戏的等级
    int  stage=1;       // 游戏的关卡

       
    接下来我们需要一个结构来记录游戏中的对象。fx和fy每次在网格上移动我们的英雄和敌人一些较小的象素,以创建一个平滑的动画效果。x和y则记录着对象处于网格的那个交点上。
    上下左右各有11个点,因此x和y可以是0到10之间的任意值。这也是我们为什么需要fx和fy的原因。考虑如果我们只能够在上下和左右方向的11个点间移动的话,我们的英雄不得不
    在各个点间跳跃前进。这样显然是不够平滑美观的。
    最后一个变量spin用来使对象在Z轴上旋转。  
       

    struct  object        // 记录游戏中的对象
    {
     int fx, fy;        // 使移动变得平滑
     int x, y;        // 当前游戏者的位置
     float spin;        // 旋转方向
    };

       

    既然我们已经为我们的玩家,敌人,甚至是秘密武器。设置了结构体,那么同样的,为了表现刚刚创设的结构体的功能和特性,我们也可以为此设置新的结构体。
    为我们的玩家创设结构体之下的第一条直线。基本上我们将会为玩家提供fx,fy,x,y和spin值几种不同的结构体。通过增加这些直线,仅需查看玩家的x值我们就很容易取得玩家的位置,同时我们也可以通过增加玩家的旋转度来改变玩家的spin值。
    第二条直线略有不同。因为同一屏幕我们可以同时拥有至多15个敌人。我们需要为每个敌人创造上面所提到的可变量。我们通过设置一个有15个敌人的组来实现这个目标,如第一个敌人的位置被设定为敌人(0).x.第二个敌人的位置为(1),x等等
    第三条直线使得为宝物创设结构体实现了可能。宝物是一个会时不时在屏幕上出现的沙漏。我们需要通过沙漏来追踪x和y值。但是因为沙漏的位置是固定的所以我们不需要寻找最佳位置,而通过为程序后面的其他物品寻找好的可变量来实现(如fx和fy)  
       

    struct object player;        // 玩家信息
    struct object enemy[9];       // 最多9个敌人的信息
    struct object hourglass;       // 宝物信息

       
    现在我们创建一个描述时间的结构,使用这个结构我们可以很轻松的跟踪时间变量。
    接下来的第一步,就是创建一个64位的频率变量,它记录时间的频率。

    resolution变量用来记录最小的时间间隔。

    mm_timer_start和mm_timer_elapsed保存计时器开始时的时间和计时器开始后流失的时间。这两个变量只有当计算机不拥有performance counter时才启用。

    变量performance_timer用来标识计算机是否有performance counter

    如果performance counter启用,最后两个变量用来保存计时器开始时的时间和计时器开始后流失的时间,它们比普通的根据精确。
      
       

    struct           // 保存时间信息的结构
    {
      __int64       frequency;       // 频率
      float         resolution;       // 时间间隔
      unsigned long mm_timer_start;       // 多媒体计时器的开始时间
      unsigned long mm_timer_elapsed;      // 多媒体计时器的开始时间
      bool  performance_timer;      // 使用Performance Timer?
      __int64       performance_timer_start;     // Performance Timer计时器的开始时间
      __int64       performance_timer_elapsed;     // Performance Timer计时器的开始时间
    } timer;         

       
    下一行代码定义了速度表。如前所说,对象移动的速度依赖于值adjust,而以adjust为下标去检索速度表,就可以获得对象的移动速度。  
       

    int  steps[6]={ 1, 2, 4, 5, 10, 20 };    // 用来调整显示的速度

       
    接下来我们将为纹理分配空间。纹理一共2张,一张是背景而另外一张是一张字体纹理。如本系列教程中的其他课程一样,base用来指出字符显示列表的基,同样的我们在最后声明了窗口过程WndProc()。  
       

    GLuint  texture[2];       // 字符纹理
    GLuint  base;        // 字符显示列表的开始值

    LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);    

       
    接下来会是很有趣的工作。接下来的一段代码会初始化我们的计时器。代码会检查performance counter(非常精确的计数器)是否可用,如果不可用,则使用多媒体计时器。这段代码是可以移植的。  
       

    void TimerInit(void)        // 初始化我们的计时器
    {
     memset(&timer, 0, sizeof(timer));     // 清空计时器结构

     // 检测Performance Counter是否可用,可用则创建
     if (!QueryPerformanceFrequency((LARGE_INTEGER *) &timer.frequency))
     {
      // 如果不可用
      timer.performance_timer = FALSE;    // 设置Performance Timer为false
      timer.mm_timer_start = timeGetTime();   // 使用普通的计时器
      timer.resolution = 1.0f/1000.0f;    // 设置单位为毫秒
      timer.frequency  = 1000;     // 设置频率为1000
      timer.mm_timer_elapsed = timer.mm_timer_start;   // 设置流失的时间为当前的时间
     }

       
    如果performance counter 可用,则执行下面的代码:  
       

     else
     {
      // 使用Performance Counter计时器
      QueryPerformanceCounter((LARGE_INTEGER *) &timer.performance_timer_start);
      timer.performance_timer  = TRUE;    // 设置Performance Timer为TRUE
      // 计算计时的精确度
      timer.resolution  = (float) (((double)1.0f)/((double)timer.frequency));
      // 设置流失的时间为当前的时间
      timer.performance_timer_elapsed = timer.performance_timer_start;
     }
    }

       
    上面的代码设置了计时器,而下面的代码则读出计时器并返回已经经过的时间,以毫秒计。代码很简单,首先检查是否支持performance counter,若支持,则调用其相关函数;否则调用多媒体函数。  
       

    float TimerGetTime()        // 返回经过的时间,以毫秒为单位
    {
     __int64 time;        // 使用64位的整数

     if (timer.performance_timer)      // 是否使用Performance Timer计时器?
     {
      QueryPerformanceCounter((LARGE_INTEGER *) &time);  // 返回当前的时间
      // 返回时间差
      return ( (float) ( time - timer.performance_timer_start) * timer.resolution)*1000.0f;
     }
     else
     {
      // 使用普通的计时器,返回时间差
      return( (float) ( timeGetTime() - timer.mm_timer_start) * timer.resolution)*1000.0f;
     }
    }

       
    在下面的代码里,我们把玩家重置在屏幕的左上角,而给敌人设置一个随机的位置。  
       

    void ResetObjects(void)        // 重置玩家和敌人
    {
     player.x=0;        // 把玩家置于左上角
     player.y=0;        
     player.fx=0;        
     player.fy=0;        

       
    接着我们给敌人一个随机的开始位置,敌人的数量等于难度乘上当前关卡号。记着,难度最大是3,而最多有3关。因此敌人最多有9个。  
       

     for (loop1=0; loop1<(stage*level); loop1++)    // 循环随即放置所有的敌人
     {
      enemy[loop1].x=5+rand()%6;     
      enemy[loop1].y=rand()%11;     
      enemy[loop1].fx=enemy[loop1].x*60;    
      enemy[loop1].fy=enemy[loop1].y*40;    
     }
    }

       
    并没有做任何改动,因此我将跳过它。在LoadGLTextures函数里我将载入那两个纹理--背景和字体。并且我会把这两副图都转化成纹理,这样我们就可以在游戏中使用他们。纹理创建好之后,象素数据就可以删除了。没有什么新东西,你可以阅读以前的课程以获得更多信息。
      
       

    int LoadGLTextures()        
    {
     int Status=FALSE;       
     AUX_RGBImageRec *TextureImage[2];     
     memset(TextureImage,0,sizeof(void *)*2);    
     if  ((TextureImage[0]=LoadBMP("Data/Font.bmp")) &&   // 载入字体纹理
       (TextureImage[1]=LoadBMP("Data/Image.bmp")))   // 载入图像纹理
     {
      Status=TRUE;       

      glGenTextures(2, &texture[0]);     

      for (loop1=0; loop1<2; loop1++)     
      {
       glBindTexture(GL_TEXTURE_2D, texture[loop1]);
       glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[loop1]->sizeX, TextureImage[loop1]->sizeY,
        0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[loop1]->data);
       glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
       glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
      }

      for (loop1=0; loop1<2; loop1++)     
      {
       if (TextureImage[loop1])    
       {
        if (TextureImage[loop1]->data)   
        {
         free(TextureImage[loop1]->data); 
        }
        free(TextureImage[loop1]);   
       }
      }
     }
     return Status;       
    }

       
    下面的代码建立了显示列表。对于字体的显示,我已经写过教程。在这里我把字体图象分成16×16个单元共256个字符。如果你有什么不明白,请参阅前面的教程  
       

    GLvoid BuildFont(GLvoid)       
    {
     base=glGenLists(256);       
     glBindTexture(GL_TEXTURE_2D, texture[0]);    
     for (loop1=0; loop1<256; loop1++)     
     {
      float cx=float(loop1%16)/16.0f;     
      float cy=float(loop1/16)/16.0f;     

      glNewList(base+loop1,GL_COMPILE);    
       glBegin(GL_QUADS);     
        glTexCoord2f(cx,1.0f-cy-0.0625f);  
        glVertex2d(0,16);    
        glTexCoord2f(cx+0.0625f,1.0f-cy-0.0625f); 
        glVertex2i(16,16);    
        glTexCoord2f(cx+0.0625f,1.0f-cy);  
        glVertex2i(16,0);    
        glTexCoord2f(cx,1.0f-cy);   
        glVertex2i(0,0);    
       glEnd();      
       glTranslated(15,0,0);     
      glEndList();       
     }         
    }

       
    当我们不再需要显示列表的时候,销毁它是一个好主意。在这里我仍然把代码加上了,虽然没有什么新东西。  
       

    GLvoid KillFont(GLvoid)  
    {
     glDeleteLists(base,256);      
    }

       
    函数没有做太多改变。唯一的改动是它可以打印变量了。我把代码列出这样你可以容易看到改动的地方。
    请注意,在这里我激活了纹理并且重置了视图矩阵。如果set被置1的话,字体将被放大。我这样做是希望可以在屏幕上显示大一点的字符。在一切结束后,我会禁用纹理。  
       

    GLvoid glPrint(GLint x, GLint y, int set, const char *fmt, ...)
    {
     char  text[256];      
     va_list  ap;       

     if (fmt == NULL)  
      return;        

     va_start(ap, fmt);       
         vsprintf(text, fmt, ap);      
     va_end(ap);        

     if (set>1)        
     {
      set=1;       
     }
     glEnable(GL_TEXTURE_2D);      
     glLoadIdentity();       
     glTranslated(x,y,0);       
     glListBase(base-32+(128*set));      

     if (set==0)        
     {
      glScalef(1.5f,2.0f,1.0f);     
     }

     glCallLists(strlen(text),GL_UNSIGNED_BYTE, text);   
     glDisable(GL_TEXTURE_2D);      
    }

       
    下面的代码基本没有变化,只是把透视投影变为了正投影  
       

    GLvoid ReSizeGLScene(GLsizei width, GLsizei height)    
    {
     if (height==0)       
     {
      height=1;       
     }

     glViewport(0,0,width,height);      

     glMatrixMode(GL_PROJECTION);      
     glLoadIdentity();       

     glOrtho(0.0f,width,height,0.0f,-1.0f,1.0f);

     glMatrixMode(GL_MODELVIEW);      
     glLoadIdentity();       
    }

       
    初始化的代码和前面的代码相比没有什么改变  
       

    int InitGL(GLvoid)
    {
     if (!LoadGLTextures())       
     {
      return FALSE;       
     }

     BuildFont();        

     glShadeModel(GL_SMOOTH);      
     glClearColor(0.0f, 0.0f, 0.0f, 0.5f);     
     glClearDepth(1.0f);       
     glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);     
     glEnable(GL_BLEND);       
     glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
     return TRUE;        
    }

       
    下面是我们的绘制代码。
    首先我们清空缓存,接着绑定字体的纹理,绘制游戏的提示字符串
      
       

    int DrawGLScene(GLvoid)        
    {
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);   
     glBindTexture(GL_TEXTURE_2D, texture[0]);    // 选择字符纹理
     glColor3f(1.0f,0.5f,1.0f);      
     glPrint(207,24,0,"GRID CRAZY");      // 绘制游戏名称"GRID CRAZY"
     glColor3f(1.0f,1.0f,0.0f);      
     glPrint(20,20,1,"Level:%2i",level2);     // 绘制当前的级别
     glPrint(20,40,1,"Stage:%2i",stage);     // 绘制当前级别的关卡

       
    现在我们检测游戏是否结束,如果游戏结束绘制"Gmae over"并提示玩家按空格键重新开始  
       

     if (gameover)        // 游戏是否结束?
     {
      glColor3ub(rand()%255,rand()%255,rand()%255);   // 随机选择一种颜色
      glPrint(472,20,1,"GAME OVER");     // 绘制 GAME OVER 字符串到屏幕
      glPrint(456,40,1,"PRESS SPACE");    // 提示玩家按空格键重新开始
     }

       
    在屏幕的右上角绘制玩家的剩余生命  
       

     for (loop1=0; loop1<lives-1; loop1++)     //循环绘制玩家的剩余生命
     {
      glLoadIdentity();      
      glTranslatef(490+(loop1*40.0f),40.0f,0.0f);   // 移动到屏幕右上角
      glRotatef(-player.spin,0.0f,0.0f,1.0f);    // 旋转绘制的生命图标
      glColor3f(0.0f,1.0f,0.0f);     // 绘制玩家生命
      glBegin(GL_LINES);      // 绘制玩家图标
       glVertex2d(-5,-5);     
       glVertex2d( 5, 5);     
       glVertex2d( 5,-5);     
       glVertex2d(-5, 5);     
      glEnd();       
      glRotatef(-player.spin*0.5f,0.0f,0.0f,1.0f);   
      glColor3f(0.0f,0.75f,0.0f);     
      glBegin(GL_LINES);      
       glVertex2d(-7, 0);     
       glVertex2d( 7, 0);     
       glVertex2d( 0,-7);     
       glVertex2d( 0, 7);     
      glEnd();       
     }

       
    下面我们来绘制网格,我们设置变量filled为TRUE,这告诉程序填充网格。
    接着我们把线的宽度设置为2,并把线的颜色设置为蓝色,接着我们检测线断是否被走过,如果走过我们设置颜色为白色。

      
       

     filled=TRUE;        // 在测试前,把填充变量设置为TRUE
     glLineWidth(2.0f);       // 设置线宽为2.0f
     glDisable(GL_LINE_SMOOTH);      // 禁用反走样
     glLoadIdentity();       
     for (loop1=0; loop1<11; loop1++)     // 循环11根线
     {
      for (loop2=0; loop2<11; loop2++)    // 循环每根线的线段
      {
       glColor3f(0.0f,0.5f,1.0f);    // 设置线为蓝色
       if (hline[loop1][loop2])    // 是否走过?
       {
        glColor3f(1.0f,1.0f,1.0f);   // 是,设线为白色
       }
       if (loop1<10)      // 绘制水平线
       {
        if (!hline[loop1][loop2])   // 如果当前线段没有走过,则不填充
        {
         filled=FALSE;    
        }
        glBegin(GL_LINES);    // 绘制当前的线段
         glVertex2d(20+(loop1*60),70+(loop2*40)); 
         glVertex2d(80+(loop1*60),70+(loop2*40)); 
        glEnd();     
       }

       
    下面的代码绘制垂直的线段  
       

       glColor3f(0.0f,0.5f,1.0f);    // 设置线为蓝色
       if (vline[loop1][loop2])    // 是否走过
       {
        glColor3f(1.0f,1.0f,1.0f);   // 是,设线为白色
       }
       if (loop2<10)      // 绘制垂直线
       {
        if (!vline[loop1][loop2])   // 如果当前线段没有走过,则不填充
        {
         filled=FALSE;    
        }
        glBegin(GL_LINES);    // 绘制当前的线段
         glVertex2d(20+(loop1*60),70+(loop2*40)); 
         glVertex2d(20+(loop1*60),110+(loop2*40)); 
        glEnd();     
       }

       
    接下来我们检测长方形的四个边是否都被走过,如果被走过我们就绘制一个带纹理的四边形。
    我们用下图来解释这个检测过程


    如果对于垂直线vline的相邻两个边都被走过,并且水平线hline的相邻两个边也被走过,那么我们就可以绘制这个四边形了。我们可以使用循环检测每一个四边形,代码如下:  
       

       glEnable(GL_TEXTURE_2D);    // 使用纹理映射
       glColor3f(1.0f,1.0f,1.0f);    // 设置为白色
       glBindTexture(GL_TEXTURE_2D, texture[1]);  // 绑定纹理
       if ((loop1<10) && (loop2<10))    // 绘制走过的四边形
       {
        // 这个四边形是否被走过?
        if (hline[loop1][loop2] && hline[loop1][loop2+1] && vline[loop1][loop2] && vline[loop1+1][loop2])
        {
         glBegin(GL_QUADS);   // 是,则绘制它
          glTexCoord2f(float(loop1/10.0f)+0.1f,1.0f-(float(loop2/10.0f)));
          glVertex2d(20+(loop1*60)+59,(70+loop2*40+1)); 
          glTexCoord2f(float(loop1/10.0f),1.0f-(float(loop2/10.0f)));
          glVertex2d(20+(loop1*60)+1,(70+loop2*40+1)); 
          glTexCoord2f(float(loop1/10.0f),1.0f-(float(loop2/10.0f)+0.1f));
          glVertex2d(20+(loop1*60)+1,(70+loop2*40)+39); 
          glTexCoord2f(float(loop1/10.0f)+0.1f,1.0f-(float(loop2/10.0f)+0.1f));
          glVertex2d(20+(loop1*60)+59,(70+loop2*40)+39); 
         glEnd();    
        }
       }
       glDisable(GL_TEXTURE_2D);    
      }
     }
     glLineWidth(1.0f);       

       
    下面的代码用来设置是否启用直线反走样  
       

     if (anti)        // 是否启用反走样?
     {
      glEnable(GL_LINE_SMOOTH);     
     }

       
    为了使游戏变得简单些,我添加了一个时间停止器,当你吃掉它时,可以让追击的你的敌人停下来。
    下面的代码用来绘制一个时间停止器。
      
       

     if (hourglass.fx==1)       
     {
      glLoadIdentity();      
      glTranslatef(20.0f+(hourglass.x*60),70.0f+(hourglass.y*40),0.0f); 
      glRotatef(hourglass.spin,0.0f,0.0f,1.0f);   
      glColor3ub(rand()%255,rand()%255,rand()%255);   
      glBegin(GL_LINES);
       glVertex2d(-5,-5);     
       glVertex2d( 5, 5);     
       glVertex2d( 5,-5);     
       glVertex2d(-5, 5);     
       glVertex2d(-5, 5);     
       glVertex2d( 5, 5);     
       glVertex2d(-5,-5);     
       glVertex2d( 5,-5);     
      glEnd();       
     }

       
    接下来绘制我们玩家  
       

     glLoadIdentity();
     glTranslatef(player.fx+20.0f,player.fy+70.0f,0.0f);   // 设置玩家的位置
     glRotatef(player.spin,0.0f,0.0f,1.0f);     // 旋转动画
     glColor3f(0.0f,1.0f,0.0f);      
     glBegin(GL_LINES);       
      glVertex2d(-5,-5);      
      glVertex2d( 5, 5);      
      glVertex2d( 5,-5);      
      glVertex2d(-5, 5);      
     glEnd();        
       
    绘制玩家的显示效果,让它看起来更好看些(其实没用)  
       

     glRotatef(player.spin*0.5f,0.0f,0.0f,1.0f);
     glColor3f(0.0f,0.75f,0.0f);      
     glBegin(GL_LINES);       
      glVertex2d(-7, 0);      
      glVertex2d( 7, 0);      
      glVertex2d( 0,-7);      
      glVertex2d( 0, 7);      
     glEnd();       
       
    接下来绘制追击玩家的敌人  
       

     for (loop1=0; loop1<(stage*level); loop1++)
     {
      glLoadIdentity();      
      glTranslatef(enemy[loop1].fx+20.0f,enemy[loop1].fy+70.0f,0.0f);
      glColor3f(1.0f,0.5f,0.5f);     
      glBegin(GL_LINES);      
       glVertex2d( 0,-7);     
       glVertex2d(-7, 0);     
       glVertex2d(-7, 0);     
       glVertex2d( 0, 7);     
       glVertex2d( 0, 7);     
       glVertex2d( 7, 0);     
       glVertex2d( 7, 0);     
       glVertex2d( 0,-7);     
      glEnd();       
       
    下面的代码绘制敌人的显示效果,让其更好看。  
       

      glRotatef(enemy[loop1].spin,0.0f,0.0f,1.0f);   
      glColor3f(1.0f,0.0f,0.0f);     
      glBegin(GL_LINES);      
       glVertex2d(-7,-7);     
       glVertex2d( 7, 7);     
       glVertex2d(-7, 7);     
       glVertex2d( 7,-7);     
      glEnd();       
     }
     return TRUE;        
    }

       
    KillGLWindow函数基本没有变化,只在最后一行添加KillFont函数  
       

    GLvoid KillGLWindow(GLvoid)       
    {
     if (fullscreen)       
     {
      ChangeDisplaySettings(NULL,0);     
      ShowCursor(TRUE);      
     }

     if (hRC)        
     {
      if (!wglMakeCurrent(NULL,NULL))     
      {
       MessageBox(NULL,"Release Of DC And RC Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
      }

      if (!wglDeleteContext(hRC))     
      {
       MessageBox(NULL,"Release Rendering Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
      }
      hRC=NULL;       
     }

     if (hDC && !ReleaseDC(hWnd,hDC))     
     {
      MessageBox(NULL,"Release Device Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
      hDC=NULL;       
     }

     if (hWnd && !DestroyWindow(hWnd))     
     {
      MessageBox(NULL,"Could Not Release hWnd.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
      hWnd=NULL;       
     }

     if (!UnregisterClass("OpenGL",hInstance))    
     {
      MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
      hInstance=NULL;       
     }

     KillFont();        // 删除创建的字体
    }

       
    函数CreateGLWindow() and WndProc() 没有变化。

    游戏控制在WinMain中完成的
      
       

    int WINAPI WinMain( HINSTANCE hInstance,    
       HINSTANCE hPrevInstance,    
       LPSTR  lpCmdLine,    
       int  nCmdShow)    
    {
     MSG msg;        
     BOOL done=FALSE;       

     
     if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)
     {
      fullscreen=FALSE;      
     }

       
    在创建完OpenGL窗口后,我们添加如下的代码,它用来创建玩家和敌人,并初始化时间计时器  
       

     if (!CreateGLWindow("NeHe's Line Tutorial",640,480,16,fullscreen)) 
     {
      return 0;       
     }

     ResetObjects();        // 重置玩家和敌人
     TimerInit();        // 初始化时间计时器

     while(!done)        
     {
      if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))   
      {
       if (msg.message==WM_QUIT)    
       {
        done=TRUE;     
       }
       else       
       {
        TranslateMessage(&msg);    
        DispatchMessage(&msg);    
       }
      }
      else        
      {

       
    接下来取得当前的时间,并在速度快的机器上让其空循环,使得程序在所有的机器上都拥有同样的帧率  
       

       float start=TimerGetTime();    // 返回当前的时间

       if ((active && !DrawGLScene()) || keys[VK_ESCAPE]) 
       {
        done=TRUE;     
       }
       else       
       {
        SwapBuffers(hDC);    
       }

       while(TimerGetTime()<start+float(steps[adjust]*2.0f)) {}// 速度快的机器上让其空循环

       
    下面的部分没有改变,按F1执行窗口和全屏的切换  
       

       if (keys[VK_F1])
       {
        keys[VK_F1]=FALSE;    
        KillGLWindow();     
        fullscreen=!fullscreen;    
        if (!CreateGLWindow("NeHe's Line Tutorial",640,480,16,fullscreen))
        {
         return 0;    
        }
       }

       
    按A键切换是否启用反走样  
       

       if (keys['A'] && !ap)     // 如果'A' 键被按下,启用反走样
       {
        ap=TRUE;     
        anti=!anti;     
       }
       if (!keys['A'])      
       {
        ap=FALSE;     
       }

       
    如果游戏没有结束,执行游戏循环  
       

       if (!gameover && active)    // 如果游戏没有结束,则进行游戏循环
       {
        for (loop1=0; loop1<(stage*level); loop1++) // 循环不同的难度等级
        {

       
    根据玩家的位置,让敌人追击玩家  
       

         if ((enemy[loop1].x<player.x) && (enemy[loop1].fy==enemy[loop1].y*40))
         {
          enemy[loop1].x++;  
         }

         if ((enemy[loop1].x>player.x) && (enemy[loop1].fy==enemy[loop1].y*40))
         {
          enemy[loop1].x--;  
         }

         if ((enemy[loop1].y<player.y) && (enemy[loop1].fx==enemy[loop1].x*60))
         {
          enemy[loop1].y++;  
         }

         if ((enemy[loop1].y>player.y) && (enemy[loop1].fx==enemy[loop1].x*60))
         {
          enemy[loop1].y--;  
         }

       
    如果时间停止器的显示时间结束,而玩家又没有吃到,那么重置计时计算器。  
       

         if (delay>(3-level) && (hourglass.fx!=2))  // 如果没有吃到时间停止器
         {
          delay=0;     // 重置时间停止器
          for (loop2=0; loop2<(stage*level); loop2++) // 循环设置每个敌人的位置
          {

       
    下面的代码调整每个敌人的位置,并绘制它们的显示效果  
       

           if (enemy[loop2].fx<enemy[loop2].x*60) 
           {
            enemy[loop2].fx+=steps[adjust]; 
            enemy[loop2].spin+=steps[adjust]; 
           }
           if (enemy[loop2].fx>enemy[loop2].x*60) 
           {
            enemy[loop2].fx-=steps[adjust]; 
            enemy[loop2].spin-=steps[adjust]; 
           }
           if (enemy[loop2].fy<enemy[loop2].y*40) 
           {
            enemy[loop2].fy+=steps[adjust]; 
            enemy[loop2].spin+=steps[adjust]; 
           }
           if (enemy[loop2].fy>enemy[loop2].y*40) 
           {
            enemy[loop2].fy-=steps[adjust]; 
            enemy[loop2].spin-=steps[adjust]; 
           }
          }
         }

       
    如果敌人的位置和玩家的位置相遇,这玩家死亡,开始新的一局  
       

         // 敌人的位置和玩家的位置相遇?
         if ((enemy[loop1].fx==player.fx) && (enemy[loop1].fy==player.fy))
         {
          lives--;   // 如果是,生命值减1

          if (lives==0)   // 如果生命值为0,则游戏结束
          {
           gameover=TRUE;  
          }

          ResetObjects();   // 重置所有的游戏变量
          PlaySound("Data/Die.wav", NULL, SND_SYNC); // 播放死亡的音乐
         }
        }

       
    使用上,下,左,右控制玩家的位置  
       

        if (keys[VK_RIGHT] && (player.x<10) && (player.fx==player.x*60) && (player.fy==player.y*40))
        {
         hline[player.x][player.y]=TRUE;  
         player.x++;    
        }
        if (keys[VK_LEFT] && (player.x>0) && (player.fx==player.x*60) && (player.fy==player.y*40))
        {
         player.x--;    
         hline[player.x][player.y]=TRUE;  
        }
        if (keys[VK_DOWN] && (player.y<10) && (player.fx==player.x*60) && (player.fy==player.y*40))
        {
         vline[player.x][player.y]=TRUE;  
         player.y++;    
        }
        if (keys[VK_UP] && (player.y>0) && (player.fx==player.x*60) && (player.fy==player.y*40))
        {
         player.y--;    
         vline[player.x][player.y]=TRUE;  
        }

       
    调整玩家的位置,让动画看起来跟自然  
       

        if (player.fx<player.x*60)   
        {
         player.fx+=steps[adjust];  
        }
        if (player.fx>player.x*60)   
        {
         player.fx-=steps[adjust];  
        }
        if (player.fy<player.y*40)   
        {
         player.fy+=steps[adjust];  
        }
        if (player.fy>player.y*40)   
        {
         player.fy-=steps[adjust];  
        }
       }

       
    如果游戏结束,按空格开始新的一局游戏  
       

       else       // 如果游戏结束
       {
        if (keys[' '])     // 按下空格?
        {
         gameover=FALSE;    // 开始新的一局
         filled=TRUE;    // 重置所有的变量
         level=1;    
         level2=1;   
         stage=0;    
         lives=5;    
        }
       }

       
    如果顺利通过本关,播放通关音乐,并提高游戏难度,开始新的一局  
       

       if (filled)      // 所有网格是否填满
       {
        PlaySound("Data/Complete.wav", NULL, SND_SYNC); // 播放过关音乐
        stage++;     // 增加游戏难度
        if (stage>3)     // 如果当前的关卡大于3,则进入到下一个大的关卡?
        {
         stage=1;    // 重置当前的关卡
         level++;    // 增加大关卡的值
         level2++;    
         if (level>3)    
         {
          level=3;   // 如果大关卡大于3,则不再增加
          lives++;   // 完成一局给玩家奖励一条生命
          if (lives>5)   // 如果玩家有5条生命,则不再增加
          {
           lives=5;  
          }
         }
        }

       
    进入到下一关卡,重置所有的游戏变量  
       

        ResetObjects();     

        for (loop1=0; loop1<11; loop1++)
        {
         for (loop2=0; loop2<11; loop2++) 
         {
          if (loop1<10)   
          {
           hline[loop1][loop2]=FALSE; 
          }
          if (loop2<10)   
          {
           vline[loop1][loop2]=FALSE; 
          }
         }
        }
       }

       
    如果玩家吃到时间停止器,记录这一信息  
       

       if ((player.fx==hourglass.x*60) && (player.fy==hourglass.y*40) && (hourglass.fx==1))
       {
        // 播放一段声音
        PlaySound("Data/freeze.wav", NULL, SND_ASYNC | SND_LOOP);
        hourglass.fx=2;     // 设置fx为2,表示吃到时间停止器
        hourglass.fy=0;     // 设置fy为0
       }

       
    显示玩家的动画效果  
       

       player.spin+=0.5f*steps[adjust];   // 旋转动画
       if (player.spin>360.0f)     
       {
        player.spin-=360;    
       }

       
    显示时间停止器的动画  
       

       hourglass.spin-=0.25f*steps[adjust];   // 旋转动画
       if (hourglass.spin<0.0f)    
       {
        hourglass.spin+=360.0f;    
       }

       
    下面的代码计算何时出现一个时间停止计数器  
       

       hourglass.fy+=steps[adjust];    // 增加fy的值,当他大于一定的时候,产生时间停止计数器
       if ((hourglass.fx==0) && (hourglass.fy>6000/level)) 
       {       
        PlaySound("Data/hourglass.wav", NULL, SND_ASYNC); 
        hourglass.x=rand()%10+1;   
        hourglass.y=rand()%11;    
        hourglass.fx=1;     //fx=1表示时间停止器出现     
        hourglass.fy=0;     
       }

       
    如果玩家没有拾取时间停止器,则过一段时间后,它自动消失  
       

       if ((hourglass.fx==1) && (hourglass.fy>6000/level)) 
       {    
        hourglass.fx=0;     // 消失后重置时间停止器
        hourglass.fy=0;     
       }

       
    如果玩家吃到时间停止器,在时间停止停止阶段播放一段音乐,过一段时间停止播放音乐  
       

       if ((hourglass.fx==2) && (hourglass.fy>500+(500*level)))
       {       
        PlaySound(NULL, NULL, 0);   // 停止播放音乐
        hourglass.fx=0;     // 重置变量
        hourglass.fy=0;     
       }

       
    增加敌人的延迟计数器的值,这个值用来更新敌人的运动
      
       

       delay++;      // 增加敌人的延迟计数器的值
      }
     }

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

       
    我花了很长时间写这份教程,它开始于一个简单的直线教程,结束与一个小型的游戏。希望它能给你一些有用的信息,我知道你们中大部分喜欢那些基于“贴图”的游戏,但我觉得这些将教会你关于游戏更多的东西。如果你不同意我的看法,请让我知道,因为我想写最好的OpenGL教程。
    请注意,这是一个很大的程序了。我尽量去注释每一行代码,我知道程序运行的一切细节,但把它表达出来又是另一回事。如果你有更好的表达能力,请告诉我如何更好的表达。我希望通过我们的努力,这份教程越来越好。谢谢


       收藏   分享  
    顶(0)
      




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

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

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给一分之千发送一个短消息 把一分之千加入好友 查看一分之千的个人资料 搜索一分之千在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看一分之千的博客2
    发贴心情 
    Lesson 21
       
    Welcome to my 21st OpenGL Tutorial! Coming up with a topic for this tutorial was extremely difficult. I know alot of you are tired of learning the basics. Everyone is dying to learn about 3D objects, Multitexturing and all that other good stuff. For those people, I'm sorry, but I want to keep the learning curve gradual. Once I've gone a step ahead it's not as easy to take a step back without people losing interest. So I'd prefer to keep pushing forward at a steady pace.

    In case I've lost a few of you :) I'll tell you a bit about this tutorial. Until now all of my tutorials have used polygons, quads and triangles. So I decided it would be nice to write a tutorial on lines. A few hours after starting the line tutorial, I decided to call it quits. The tutorial was coming along fine, but it was BORING! Lines are great, but there's only so much you can do to make lines exciting. I read through my email, browsed through the message board, and wrote down a few of your tutorial requests. Out of all the requests there were a few questions that came up more than others. So... I decided to write a multi-tutorial :)

    In this tutorial you will learn about: Lines, Anti-Aliasing, Orthographic Projection, Timing, Basic Sound Effects, and Simple Game Logic. Hopefully there's enough in this tutorial to keep everyone happy :) I spent 2 days coding this tutorial, and It's taken almost 2 weeks to write this HTML file. I hope you enjoy my efforts!

    At the end of this tutorial you will have made a simple 'amidar' type game. Your mission is to fill in the grid without being caught by the bad guys. The game has levels, stages, lives, sound, and a secret item to help you progress through the levels when things get tough. Although this game will run fine on a Pentium 166 with a Voodoo 2, a faster processor is recommended if you want smoother animation.

    I used the code from lesson 1 as a starting point while writing this tutorial. We start off by adding the required header files. stdio.h is used for file operations, and we include stdarg.h so that we can display variables on the screen, such as the score and current stage.   
       

    // This Code Was Created By Jeff Molofee 2000
    // If You've Found This Code Useful, Please Let Me Know.

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

       
    Now we set up our boolean variables. vline keeps track of the 121 vertical lines that make up our game grid. 11 lines across and 11 up and down. hline keeps track of the 121 horizontal lines that make up the game grid. We use ap to keep track of whether or not the 'A' key is being pressed.

    filled is FALSE while the grid isn't filled and TRUE when it's been filled in. gameover is pretty obvious. If gameover is TRUE, that's it, the game is over, otherwise you're still playing. anti keeps track of antialiasing. If anti is TRUE, object antialiasing is ON. Otherwise it's off. active and fullscreen keep track of whether or not the program has been minimized or not, and whether you're running in fullscreen mode or windowed mode.   
       

    bool  keys[256];       // Array Used For The Keyboard Routine
    bool  vline[11][10];       // Keeps Track Of Verticle Lines
    bool  hline[10][11];       // Keeps Track Of Horizontal Lines
    bool  ap;        // 'A' Key Pressed?
    bool  filled;        // Done Filling In The Grid?
    bool  gameover;       // Is The Game Over?
    bool  anti=TRUE;       // Antialiasing?
    bool  active=TRUE;       // Window Active Flag Set To TRUE By Default
    bool  fullscreen=TRUE;      // Fullscreen Flag Set To Fullscreen Mode By Default

       
    Now we set up our integer variables. loop1 and loop2 will be used to check points on our grid, see if an enemy has hit us and to give objects random locations on the grid. You'll see loop1 / loop2 in action later in the program. delay is a counter variable that I use to slow down the bad guys. If delay is greater than a certain value, the enemies are moved and delay is set back to zero.

    The variable adjust is a very special variable! Even though this program has a timer, the timer only checks to see if your computer is too fast. If it is, a delay is created to slow the computer down. On my GeForce card, the program runs insanely smooth, and very very fast. After testing this program on my PIII/450 with a Voodoo 3500TV, I noticed that the program was running extremely slow. The problem is that my timing code only slows down the gameplay. It wont speed it up. So I made a new variable called adjust. adjust can be any value from 0 to 5. The objects in the game move at different speeds depending on the value of adjust. The lower the value the smoother they move, the higher the value, the faster they move (choppy at values higher than 3). This was the only real easy way to make the game playable on slow systems. One thing to note, no matter how fast the objects are moving the game speed will never run faster than I intended it to run. So setting the adjust value to 3 is safe for fast and slow systems.

    The variable lives is set to 5 so that you start the game with 5 lives. level is an internal variable. The game uses it to keep track of the level of difficulty. This is not the level that you will see on the screen. The variable level2 starts off with the same value as level but can increase forever depending on your skill. If you manage to get past level 3 the level variable will stop increasing at 3. The level variable is an internal variable used for game difficulty. The stage variable keeps track of the current game stage.   
       

    int  loop1;        // Generic Loop1
    int  loop2;        // Generic Loop2
    int  delay;        // Enemy Delay
    int  adjust=3;       // Speed Adjustment For Really Slow Video Cards
    int  lives=5;       // Player Lives
    int  level=1;       // Internal Game Level
    int  level2=level;       // Displayed Game Level
    int  stage=1;       // Game Stage

       
    Now we create a structure to keep track of the objects in our game. We have a fine X position (fx) and a fine Y position (fy). These variables will move the player and enemies around the grid a few pixels at a time. Creating a smooth moving object.

    Then we have x and y. These variables will keep track of what intersection our player is at. There are 11 points left and right and 11 points up and down. So x and y can be any value from 0 to 10. That is why we need the fine values. If we could only move one of 11 spots left and right and one of 11 spots up and down our player would jump around the screen in a quick (non smooth) motion.

    The last variable spin will be used to spin the objects on their z-axis.   
       

    struct  object        // Create A Structure For Our Player
    {
     int fx, fy;        // Fine Movement Position
     int x, y;        // Current Player Position
     float spin;        // Spin Direction
    };

       
    Now that we have created a structure that can be used for our player, enemies and even a special item we can create new structures that take on the characteristics of the structure we just made.

    The first line below creates a structure for our player. Basically we're giving our player structure fx, fy, x, y and spin values. By adding this line, we can access the player x position by checking player.x. We can change the player spin by adding a number to player.spin.

    The second line is a bit different. Because we can have up to 15 enemies on the screen at a time, we need to create the above variables for each enemy. We do this by making an array of 15 enemies. the x position of the first enemy will be enemy[0].x. The second enemy will be enemy[1].x, etc.

    The last line creates a structure for our special item. The special item is an hourglass that will appear on the screen from time to time. We need to keep track of the x and y values for the hourglass, but because the hourglass doesn't move, we don't need to keep track of the fine positions. Instead we will use the fine variables (fx and fy) for other things later in the program.   
       

    struct object player;        // Player Information
    struct object enemy[9];       // Enemy Information
    struct object hourglass;       // Hourglass Information

       
    Now we create a timer structure. We create a structure so that it's easier to keep track of timer variables and so that it's easier to tell that the variable is a timer variable.

    The first thing we do is create a 64 bit integer called frequency. This variable will hold the frequency of the timer. When I first wrote this program, I forgot to include this variable. I didn't realize that the frequency on one machine may not match the frequency on another. Big mistake on my part! The code ran fine on the 3 systems in my house, but when I tested it on a friends machine the game ran WAY to fast. Frequency is basically how fast the clock is updated. Good thing to keep track of :)

    The resolution variable keeps track of the steps it takes before we get 1 millisecond of time.

    mm_timer_start and mm_timer_elapsed hold the value that the timer started at, and the amount of time that has elapsed since the the timer was started. These two variables are only used if the computer doesn't have a performance counter. In that case we end up using the less accurate multimedia timer, which is still not to bad for a non-time critical game like this.

    The variable performance_timer can be either TRUE of FALSE. If the program detects a performance counter, the variable performance_timer variable is set to TRUE, and all timing is done using the performance counter (alot more accurate than the multimedia timer). If a performance counter is not found, performance_timer is set to FALSE and the multimedia timer is used for timing.

    The last 2 variables are 64 bit integer variables that hold the start time of the performance counter and the amount of time that has elapsed since the performance counter was started.

    The name of this structure is "timer" as you can see at the bottom of the structure. If we want to know the timer frequency we can now check timer.frequency. Nice!   
       

    struct           // Create A Structure For The Timer Information
    {
      __int64       frequency;       // Timer Frequency
      float         resolution;       // Timer Resolution
      unsigned long mm_timer_start;       // Multimedia Timer Start Value
      unsigned long mm_timer_elapsed;      // Multimedia Timer Elapsed Time
      bool  performance_timer;      // Using The Performance Timer?
      __int64       performance_timer_start;     // Performance Timer Start Value
      __int64       performance_timer_elapsed;     // Performance Timer Elapsed Time
    } timer;         // Structure Is Named timer

       
    The next line of code is our speed table. The objects in the game will move at a different rate depending on the value of adjust. If adjust is 0 the objects will move one pixel at a time. If the value of adjust is 5, the objects will move 20 pixels at a time. So by increasing the value of adjust the speed of the objects will increase, making the game run faster on slow computers. The higher adjust is however, the choppier the game will play.

    Basically steps[ ] is just a look-up table. If adjust was 3, we would look at the number stored at location 3 in steps[ ]. Location 0 holds the value 1, location 1 holds the value 2, location 2 holds the value 4, and location 3 hold the value 5. If adjust was 3, our objects would move 5 pixels at a time. Make sense?   
       

    int  steps[6]={ 1, 2, 4, 5, 10, 20 };    // Stepping Values For Slow Video Adjustment

       
    Next we make room for two textures. We'll load a background scene, and a bitmap font texture. Then we set up a base variable so we can keep track of our font display list just like we did in the other font tutorials. Finally we declare WndProc().   
       

    GLuint  texture[2];       // Font Texture Storage Space
    GLuint  base;        // Base Display List For The Font

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

       
    Now for the fun stuff :) The next section of code initializes our timer. It will check the computer to see if a performance counter is available (very accurate counter). If we don't have a performance counter the computer will use the multimedia timer. This code should be portable from what I'm told.

    We start off by clearing all the timer variables to zero. This will set all the variables in our timer structure to zero. After that, we check to see if there is NOT a performance counter. The ! means NOT. If there is, the frequency will be stored in timer.frequency.

    If there was no performance counter, the code in between the { }'s is run. The first line sets the variable timer.performance_timer to FALSE. This tells our program that there is no performance counter. The second line gets our starting multimedia timer value from timeGetTime(). We set the timer.resolution to 0.001f, and the timer.frequency to 1000. Because no time has elapsed yet, we make the elapsed time equal the start time.   
       

    void TimerInit(void)        // Initialize Our Timer (Get It Ready)
    {
     memset(&timer, 0, sizeof(timer));     // Clear Our Timer Structure

     // Check To See If A Performance Counter Is Available
     // If One Is Available The Timer Frequency Will Be Updated
     if (!QueryPerformanceFrequency((LARGE_INTEGER *) &timer.frequency))
     {
      // No Performace Counter Available
      timer.performance_timer = FALSE;    // Set Performance Timer To FALSE
      timer.mm_timer_start = timeGetTime();   // Use timeGetTime() To Get Current Time
      timer.resolution = 1.0f/1000.0f;    // Set Our Timer Resolution To .001f
      timer.frequency  = 1000;     // Set Our Timer Frequency To 1000
      timer.mm_timer_elapsed = timer.mm_timer_start;   // Set The Elapsed Time To The Current Time
     }

       
    If there is a performance counter, the following code is run instead. The first line grabs the current starting value of the performance counter, and stores it in timer.performance_timer_start. Then we set timer.performance_timer to TRUE so that our program knows there is a performance counter available. After that we calculate the timer resolution by using the frequency that we got when we checked for a performance counter in the code above. We divide 1 by the frequency to get the resolution. The last thing we do is make the elapsed time the same as the starting time.

    Notice instead of sharing variables for the performance and multimedia timer start and elapsed variables, I've decided to make seperate variables. Either way it will work fine.   
       

     else
     {
      // Performance Counter Is Available, Use It Instead Of The Multimedia Timer
      // Get The Current Time And Store It In performance_timer_start
      QueryPerformanceCounter((LARGE_INTEGER *) &timer.performance_timer_start);
      timer.performance_timer  = TRUE;    // Set Performance Timer To TRUE
      // Calculate The Timer Resolution Using The Timer Frequency
      timer.resolution  = (float) (((double)1.0f)/((double)timer.frequency));
      // Set The Elapsed Time To The Current Time
      timer.performance_timer_elapsed = timer.performance_timer_start;
     }
    }

       
    The section of code above sets up the timer. The code below reads the timer and returns the amount of time that has passed in milliseconds.

    The first thing we do is set up a 64 bit variable called time. We will use this variable to grab the current counter value. The next line checks to see if we have a performance counter. If we do, timer.performance_timer will be TRUE and the code right after will run.

    The first line of code inside the { }'s grabs the counter value and stores it in the variable we created called time. The second line takes the time we just grabbed (time and subtracts the start time that we got when we initialized the timer. This way our timer should start out pretty close to zero. We then multiply the results by the resolution to find out how many seconds have passed. The last thing we do is multiply the result by 1000 to figure out how many milliseconds have passed. After the calculation is done, our results are sent back to the section of code that called this procedure. The results will be in floating point format for greater accuracy.

    If we are not using the peformance counter, the code after the else statement will be run. It does pretty much the same thing. We grab the current time with timeGetTime() and subtract our starting counter value. We multiply it by our resolution and then multiply the result by 1000 to convert from seconds into milliseconds.   
       

    float TimerGetTime()        // Get Time In Milliseconds
    {
     __int64 time;        // time Will Hold A 64 Bit Integer

     if (timer.performance_timer)      // Are We Using The Performance Timer?
     {
      QueryPerformanceCounter((LARGE_INTEGER *) &time);  // Grab The Current Performance Time
      // Return The Current Time Minus The Start Time Multiplied By The Resolution And 1000 (To Get MS)
      return ( (float) ( time - timer.performance_timer_start) * timer.resolution)*1000.0f;
     }
     else
     {
      // Return The Current Time Minus The Start Time Multiplied By The Resolution And 1000 (To Get MS)
      return( (float) ( timeGetTime() - timer.mm_timer_start) * timer.resolution)*1000.0f;
     }
    }

       
    The following section of code resets the player to the top left corner of the screen, and gives the enemies a random starting point.

    The top left of the screen is 0 on the x-axis and 0 on the y-axis. So by setting the player.x value to 0 we move the player to the far left side of the screen. By setting the player.y value to 0 we move our player to the top of the screen.

    The fine positions have to be equal to the current player position, otherwise our player would move from whatever value it's at on the fine position to the top left of the screen. We don't want to player to move there, we want it to appear there, so we set the fine positions to 0 as well.   
       

    void ResetObjects(void)        // Reset Player And Enemies
    {
     player.x=0;        // Reset Player X Position To Far Left Of The Screen
     player.y=0;        // Reset Player Y Position To The Top Of The Screen
     player.fx=0;        // Set Fine X Position To Match
     player.fy=0;        // Set Fine Y Position To Match

       
    Next we give the enemies a random starting location. The number of enemies displayed on the screen will be equal to the current (internal) level value multiplied by the current stage. Remember, the maximum value that level can equal is 3 and the maximum number of stages per level is 3. So we can have a total of 9 enemies.

    To make sure we give all the viewable enemies a new position, we loop through all the visible enemies (stage times level). We set each enemies x position to 5 plus a random value from 0 to 5. (the maximum value rand can be is always the number you specify minus 1). So the enemy can appear on the grid, anywhere from 5 to 10. We then give the enemy a random value on the y axis from 0 to 10.

    We don't want the enemy to move from it's old position to the new random position so we make sure the fine x (fx) and y (fy) values are equal to the actual x and y values multiplied by width and height of each tile on the screen. Each tile has a width of 60 and a height of 40.   
       

     for (loop1=0; loop1<(stage*level); loop1++)    // Loop Through All The Enemies
     {
      enemy[loop1].x=5+rand()%6;     // Select A Random X Position
      enemy[loop1].y=rand()%11;     // Select A Random Y Position
      enemy[loop1].fx=enemy[loop1].x*60;    // Set Fine X To Match
      enemy[loop1].fy=enemy[loop1].y*40;    // Set Fine Y To Match
     }
    }

       
    The AUX_RGBImageRec code hasn't changed so I'm skipping over it. In LoadGLTextures() we will load in our two textures. First the font bitmap (Font.bmp) and then the background image (Image.bmp). We'll convert both the images into textures that we can use in our game. After we have built the textures we clean up by deleting the bitmap information. Nothing really new. If you've read the other tutorials you should have no problems understanding the code.   
       

    int LoadGLTextures()        // Load Bitmaps And Convert To Textures
    {
     int Status=FALSE;       // Status Indicator
     AUX_RGBImageRec *TextureImage[2];     // Create Storage Space For The Textures
     memset(TextureImage,0,sizeof(void *)*2);    // Set The Pointer To NULL

     if  ((TextureImage[0]=LoadBMP("Data/Font.bmp")) &&   // Load The Font
       (TextureImage[1]=LoadBMP("Data/Image.bmp")))   // Load Background Image
     {
      Status=TRUE;       // Set The Status To TRUE

      glGenTextures(2, &texture[0]);     // Create The Texture

      for (loop1=0; loop1<2; loop1++)     // Loop Through 2 Textures
      {
       glBindTexture(GL_TEXTURE_2D, texture[loop1]);
       glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[loop1]->sizeX, TextureImage[loop1]->sizeY,
        0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[loop1]->data);
       glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
       glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
      }

      for (loop1=0; loop1<2; loop1++)     // Loop Through 2 Textures
      {
       if (TextureImage[loop1])    // If Texture Exists
       {
        if (TextureImage[loop1]->data)   // If Texture Image Exists
        {
         free(TextureImage[loop1]->data); // Free The Texture Image Memory
        }
        free(TextureImage[loop1]);   // Free The Image Structure
       }
      }
     }
     return Status;        // Return The Status
    }

       
    The code below builds our font display list. I've already done a tutorial on bitmap texture fonts. All the code does is divides the Font.bmp image into 16 x 16 cells (256 characters). Each 16x16 cell will become a character. Because I've set the y-axis up so that positive goes down instead of up, it's necessary to subtract our y-axis values from 1.0f. Otherwise the letters will all be upside down :) If you don't understand what's going on, go back and read the bitmap texture font tutorial.   
       

    GLvoid BuildFont(GLvoid)       // Build Our Font Display List
    {
     base=glGenLists(256);       // Creating 256 Display Lists
     glBindTexture(GL_TEXTURE_2D, texture[0]);    // Select Our Font Texture
     for (loop1=0; loop1<256; loop1++)     // Loop Through All 256 Lists
     {
      float cx=float(loop1%16)/16.0f;     // X Position Of Current Character
      float cy=float(loop1/16)/16.0f;     // Y Position Of Current Character

      glNewList(base+loop1,GL_COMPILE);    // Start Building A List
       glBegin(GL_QUADS);     // Use A Quad For Each Character
        glTexCoord2f(cx,1.0f-cy-0.0625f);  // Texture Coord (Bottom Left)
        glVertex2d(0,16);    // Vertex Coord (Bottom Left)
        glTexCoord2f(cx+0.0625f,1.0f-cy-0.0625f); // Texture Coord (Bottom Right)
        glVertex2i(16,16);    // Vertex Coord (Bottom Right)
        glTexCoord2f(cx+0.0625f,1.0f-cy);  // Texture Coord (Top Right)
        glVertex2i(16,0);    // Vertex Coord (Top Right)
        glTexCoord2f(cx,1.0f-cy);   // Texture Coord (Top Left)
        glVertex2i(0,0);    // Vertex Coord (Top Left)
       glEnd();      // Done Building Our Quad (Character)
       glTranslated(15,0,0);     // Move To The Right Of The Character
      glEndList();       // Done Building The Display List
     }         // Loop Until All 256 Are Built
    }

       
    It's a good idea to destroy the font display list when you're done with it, so I've added the following section of code. Again, nothing new.   
       

    GLvoid KillFont(GLvoid)        // Delete The Font From Memory
    {
     glDeleteLists(base,256);      // Delete All 256 Display Lists
    }

       
    The glPrint() code hasn't changed that much. The only difference from the tutorial on bitmap font textures is that I have added the ability to print the value of variables. The only reason I've written this section of code out is so that you can see the changes. The print statement will position the text at the x and y position that you specify. You can pick one of 2 character sets, and the value of variables will be written to the screen. This allows us to display the current level and stage on the screen.

    Notice that I enable texture mapping, reset the view and then translate to the proper x / y position. Also notice that if character set 0 is selected, the font is enlarged one and half times width wise, and double it's original size up and down. I did this so that I could write the title of the game in big letters. After the text has been drawn, I disable texture mapping.   
       

    GLvoid glPrint(GLint x, GLint y, int set, const char *fmt, ...)   // Where The Printing Happens
    {
     char  text[256];      // Holds Our String
     va_list  ap;       // Pointer To List Of Arguments

     if (fmt == NULL)       // If There's No Text
      return;        // Do Nothing

     va_start(ap, fmt);       // Parses The String For Variables
         vsprintf(text, fmt, ap);      // And Converts Symbols To Actual Numbers
     va_end(ap);        // Results Are Stored In Text

     if (set>1)        // Did User Choose An Invalid Character Set?
     {
      set=1;        // If So, Select Set 1 (Italic)
     }
     glEnable(GL_TEXTURE_2D);      // Enable Texture Mapping
     glLoadIdentity();       // Reset The Modelview Matrix
     glTranslated(x,y,0);       // Position The Text (0,0 - Bottom Left)
     glListBase(base-32+(128*set));      // Choose The Font Set (0 or 1)

     if (set==0)        // If Set 0 Is Being Used Enlarge Font
     {
      glScalef(1.5f,2.0f,1.0f);     // Enlarge Font Width And Height
     }

     glCallLists(strlen(text),GL_UNSIGNED_BYTE, text);   // Write The Text To The Screen
     glDisable(GL_TEXTURE_2D);      // Disable Texture Mapping
    }

       
    The resize code is NEW :) Instead of using a perspective view I'm using an ortho view for this tutorial. That means that objects don't get smaller as they move away from the viewer. The z-axis is pretty much useless in this tutorial.

    We start off by setting up the view port. We do this the same way we'd do it if we were setting up a perspective view. We make the viewport equal to the width of our window.

    Then we select the projection matrix (thing movie projector, it information on how to display our image). and reset it.

    Immediately after we reset the projection matrix, we set up our ortho view. I'll explain the command in detail:

    The first parameter (0.0f) is the value that we want for the far left side of the screen. You wanted to know how to use actual pixel values, so instead of using a negative number for far left, I've set the value to 0. The second parameter is the value for the far right side of the screen. If our window is 640x480, the value stored in width will be 640. So the far right side of the screen effectively becomes 640. Therefore our screen runs from 0 to 640 on the x-axis.

    The third parameter (height) would normally be our negative y-axis value (bottom of the screen). But because we want exact pixels, we wont have a negative value. Instead we will make the bottom of the screen equal the height of our window. If our window is 640x480, height will be equal to 480. So the bottom of our screen will be 480. The fourth parameter would normally be the positive value for the top of our screen. We want the top of the screen to be 0 (good old fashioned screen coordinates) so we just set the fourth parameter to 0. This gives us from 0 to 480 on the y-axis.

    The last two parameters are for the z-axis. We don't really care about the z-axis so we'll set the range from -1.0f to 1.0f. Just enough that we can see anything drawn at 0.0f on the z-axis.

    After we've set up the ortho view, we select the modelview matrix (object information... location, etc) and reset it.   
       

    GLvoid ReSizeGLScene(GLsizei width, GLsizei height)    // Resize And Initialize The GL Window
    {
     if (height==0)        // Prevent A Divide By Zero By
     {
      height=1;       // Making Height Equal One
     }

     glViewport(0,0,width,height);      // Reset The Current Viewport

     glMatrixMode(GL_PROJECTION);      // Select The Projection Matrix
     glLoadIdentity();       // Reset The Projection Matrix

     glOrtho(0.0f,width,height,0.0f,-1.0f,1.0f);    // Create Ortho 640x480 View (0,0 At Top Left)

     glMatrixMode(GL_MODELVIEW);      // Select The Modelview Matrix
     glLoadIdentity();       // Reset The Modelview Matrix
    }

       
    The init code has a few new commands. We start off by loading our textures. If they didn't load properly, the program will quit with an error message. After we have built the textures, we build our font set. I don't bother error checking but you can if you want.

    After the font has been built, we set things up. We enable smooth shading, set our clear color to black and set depth clearing to 1.0f. After that is a new line of code.

    glHint() tells OpenGL how to draw something. In this case we are telling OpenGL that we want line smoothing to be the best (nicest) that OpenGL can do. This is the command that enables anti-aliasing.

    The last thing we do is enable blending and select the blend mode that makes anti-aliased lines possible. Blending is required if you want the lines to blend nicely with the background image. Disable blending if you want to see how crappy things look without it.

    It's important to point out that antialiasing may not appear to be working. The objects in this game are quite small so you may not notice the antialaising right off the start. Look hard. Notice how the jaggie lines on the enemies smooth out when antialiasing is on. The player and hourglass should look better as well.   
       

    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
     }

     BuildFont();        // Build The Font

     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_LINE_SMOOTH_HINT, GL_NICEST);     // Set Line Antialiasing
     glEnable(GL_BLEND);       // Enable Blending
     glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);   // Type Of Blending To Use
     return TRUE;        // Initialization Went OK
    }

       
    Now for the drawing code. This is where the magic happens :)

    We clear the screen (to black) along with the depth buffer. Then we select the font texture (texture[0]). We want the words "GRID CRAZY" to be a purple color so we set red and blue to full intensity, and we turn the green up half way. After we've selected the color, we call glPrint(). We position the words "GRID CRAZY" at 207 on the x axis (center on the screen) and 24 on the y-axis (up and down). We use our large font by selecting font set 0.

    After we've drawn "GRID CRAZY" to the screen, we change the color to yellow (full red, full green). We write "Level:" and the variable level2 to the screen. Remember that level2 can be greater than 3. level2 holds the level value that the player sees on the screen. %2i means that we don't want any more than 2 digits on the screen to represent the level. The i means the number is an integer number.

    After we have written the level information to the screen, we write the stage information right under it using the same color.   
       

    int DrawGLScene(GLvoid)        // Here's Where We Do All The Drawing
    {
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);   // Clear Screen And Depth Buffer
     glBindTexture(GL_TEXTURE_2D, texture[0]);    // Select Our Font Texture
     glColor3f(1.0f,0.5f,1.0f);      // Set Color To Purple
     glPrint(207,24,0,"GRID CRAZY");      // Write GRID CRAZY On The Screen
     glColor3f(1.0f,1.0f,0.0f);      // Set Color To Yellow
     glPrint(20,20,1,"Level:%2i",level2);     // Write Actual Level Stats
     glPrint(20,40,1,"Stage:%2i",stage);     // Write Stage Stats

       
    Now we check to see if the game is over. If the game is over, the variable gameover will be TRUE. If the game is over, we use glColor3ub(r,g,b) to select a random color. Notice we are using 3ub instead of 3f. By using 3ub we can use integer values from 0 to 255 to set our colors. Plus it's easier to get a random value from 0 to 255 than it is to get a random value from 0.0f to 1.0f.

    Once a random color has been selected, we write the words "GAME OVER" to the right of the game title. Right under "GAME OVER" we write "PRESS SPACE". This gives the player a visual message letting them know that they have died and to press the spacebar to restart the game.   
       

     if (gameover)        // Is The Game Over?
     {
      glColor3ub(rand()%255,rand()%255,rand()%255);   // Pick A Random Color
      glPrint(472,20,1,"GAME OVER");     // Write GAME OVER To The Screen
      glPrint(456,40,1,"PRESS SPACE");    // Write PRESS SPACE To The Screen
     }

       
    If the player still has lives left, we draw animated images of the players character to the right of the game title. To do this we create a loop that goes from 0 to the current number of lives the player has left minus one. I subtract one, because the current life is the image you control.

    Inside the loop, we reset the view. After the view has been reset, we translate to the 490 pixels to the right plus the value of loop1 times 40.0f. This draws each of the animated player lives 40 pixels apart from eachother. The first animated image will be drawn at 490+(0*40) (= 490), the second animated image will be drawn at 490+(1*40) (= 530), etc.

    After we have moved to the spot we want to draw the animated image, we rotate counterclockwise depending on the value stored in player.spin. This causes the animated life images to spin the opposite way that your active player is spinning.

    We then select green as our color, and start drawing the image. Drawing lines is alot like drawing a quad or a polygon. You start off with glBegin(GL_LINES), telling OpenGL we want to draw a line. Lines have 2 vertices. We use glVertex2d to set our first point. glVertex2d doesn't require a z value, which is nice considering we don't care about the z value. The first point is drawn 5 pixels to the left of the current x location and 5 pixels up from the current y location. Giving us a top left point. The second point of our first line is drawn 5 pixels to the right of our current x location, and 5 pixels down, giving us a bottom right point. This draws a line from the top left to the bottom right. Our second line is drawn from the top right to the bottom left. This draws a green X on the screen.

    After we have drawn the green X, we rotate counterclockwise (on the z axis) even more, but this time at half the speed. We then select a darker shade of green (0.75f) and draw another x, but we use 7 instead of 5 this time. This draws a bigger / darker x on top of the first green X. Because the darker X spins slower though, it will look as if the bright X has a spinning set of feelers (grin) on top of it.   
       

     for (loop1=0; loop1<lives-1; loop1++)     // Loop Through Lives Minus Current Life
     {
      glLoadIdentity();      // Reset The View
      glTranslatef(490+(loop1*40.0f),40.0f,0.0f);   // Move To The Right Of Our Title Text
      glRotatef(-player.spin,0.0f,0.0f,1.0f);    // Rotate Counter Clockwise
      glColor3f(0.0f,1.0f,0.0f);     // Set Player Color To Light Green
      glBegin(GL_LINES);      // Start Drawing Our Player Using Lines
       glVertex2d(-5,-5);     // Top Left Of Player
       glVertex2d( 5, 5);     // Bottom Right Of Player
       glVertex2d( 5,-5);     // Top Right Of Player
       glVertex2d(-5, 5);     // Bottom Left Of Player
      glEnd();       // Done Drawing The Player
      glRotatef(-player.spin*0.5f,0.0f,0.0f,1.0f);   // Rotate Counter Clockwise
      glColor3f(0.0f,0.75f,0.0f);     // Set Player Color To Dark Green
      glBegin(GL_LINES);      // Start Drawing Our Player Using Lines
       glVertex2d(-7, 0);     // Left Center Of Player
       glVertex2d( 7, 0);     // Right Center Of Player
       glVertex2d( 0,-7);     // Top Center Of Player
       glVertex2d( 0, 7);     // Bottom Center Of Player
      glEnd();       // Done Drawing The Player
     }

       
    Now we're going to draw the grid. We set the variable filled to TRUE. This tells our program that the grid has been completely filled in (you'll see why we do this in a second).

    Right after that we set the line width to 2.0f. This makes the lines thicker, making the grid look more defined.

    Then we disable anti-aliasing. The reason we disable anti-aliasing is because although it's a great feature, it eats CPU's for breakfast. Unless you have a killer graphics card, you'll notice a huge slow down if you leave anti-aliasing on. Go ahead and try if you want :)

    The view is reset, and we start two loops. loop1 will travel from left to right. loop2 will travel from top to bottom.

    We set the line color to blue, then we check to see if the horizontal line that we are about to draw has been traced over. If it has we set the color to white. The value of hline[loop1][loop2] will be TRUE if the line has been traced over, and FALSE if it hasn't.

    After we have set the color to blue or white, we draw the line. The first thing to do is make sure we haven't gone to far to the right. We don't want to draw any lines or check to see if the line has been filled in when loop1 is greater than 9.

    Once we are sure loop1 is in the valid range we check to see if the horizontal line hasn't been filled in. If it hasn't, filled is set to FALSE, letting our OpenGL program know that there is at least one line that hasn't been filled in.

    The line is then drawn. We draw our first horizontal (left to right) line starting at 20+(0*60) (= 20). This line is drawn all the way to 80+(0*60) (= 80). Notice the line is drawn to the right. That is why we don't want to draw 11 (0-10) lines. because the last line would start at the far right of the screen and end 80 pixels off the screen.   
       

     filled=TRUE;        // Set Filled To True Before Testing
     glLineWidth(2.0f);       // Set Line Width For Cells To 2.0f
     glDisable(GL_LINE_SMOOTH);      // Disable Antialiasing
     glLoadIdentity();       // Reset The Current Modelview Matrix
     for (loop1=0; loop1<11; loop1++)     // Loop From Left To Right
     {
      for (loop2=0; loop2<11; loop2++)    // Loop From Top To Bottom
      {
       glColor3f(0.0f,0.5f,1.0f);    // Set Line Color To Blue
       if (hline[loop1][loop2])    // Has The Horizontal Line Been Traced
       {
        glColor3f(1.0f,1.0f,1.0f);   // If So, Set Line Color To White
       }
       if (loop1<10)      // Dont Draw To Far Right
       {
        if (!hline[loop1][loop2])   // If A Horizontal Line Isn't Filled
        {
         filled=FALSE;    // filled Becomes False
        }
        glBegin(GL_LINES);    // Start Drawing Horizontal Cell Borders
         glVertex2d(20+(loop1*60),70+(loop2*40)); // Left Side Of Horizontal Line
         glVertex2d(80+(loop1*60),70+(loop2*40)); // Right Side Of Horizontal Line
        glEnd();     // Done Drawing Horizontal Cell Borders
       }

       
    The code below does the same thing, but it checks to make sure the line isn't being drawn too far down the screen instead of too far right. This code is responsible for drawing vertical lines.   
       

       glColor3f(0.0f,0.5f,1.0f);    // Set Line Color To Blue
       if (vline[loop1][loop2])    // Has The Horizontal Line Been Traced
       {
        glColor3f(1.0f,1.0f,1.0f);   // If So, Set Line Color To White
       }
       if (loop2<10)      // Dont Draw To Far Down
       {
        if (!vline[loop1][loop2])   // If A Verticle Line Isn't Filled
        {
         filled=FALSE;    // filled Becomes False
        }
        glBegin(GL_LINES);    // Start Drawing Verticle Cell Borders
         glVertex2d(20+(loop1*60),70+(loop2*40)); // Left Side Of Horizontal Line
         glVertex2d(20+(loop1*60),110+(loop2*40)); // Right Side Of Horizontal Line
        glEnd();     // Done Drawing Verticle Cell Borders
       }

       
    Now we check to see if 4 sides of a box are traced. Each box on the screen is 1/10th of a full screen picture. Because each box is piece of a larger texture, the first thing we need to do is enable texture mapping. We don't want the texture to be tinted red, green or blue so we set the color to bright white. After the color is set to white we select our grid texture (texture[1]).

    The next thing we do is check to see if we are checking a box that exists on the screen. Remember that our loop draws the 11 lines right and left and 11 lines up and down. But we dont have 11 boxes. We have 10 boxes. So we have to make sure we don't check the 11th position. We do this by making sure both loop1 and loop2 is less than 10. That's 10 boxes from 0 - 9.

    After we have made sure that we are in bounds we can start checking the borders. hline[loop1][loop2] is the top of a box. hline[loop1][loop2+1] is the bottom of a box. vline[loop1][loop2] is the left side of a box and vline[loop1+1][loop2] is the right side of a box. Hopefully I can clear things up with a diagram:


    All horizontal lines are assumed to run from loop1 to loop1+1. As you can see, the first horizontal line runs along loop2. The second horizontal line runs along loop2+1. Vertical lines are assumed to run from loop2 to loop2+1. The first vertical line runs along loop1 and the second vertical line runs along loop1+1

    When loop1 is increased, the right side of our old box becomes the left side of the new box. When loop2 is increased, the bottom of the old box becomes the top of the new box.

    If all 4 borders are TRUE (meaning we've passed over them all) we can texture map the box. We do this the same way we broke the font texture into seperate letters. We divide both loop1 and loop2 by 10 because we want to map the texture across 10 boxes from left to right and 10 boxes up and down. Texture coordinates run from 0.0f to 1.0f and 1/10th of 1.0f is 0.1f.

    So to get the top right side of our box we divide the loop values by 10 and add 0.1f to the x texture coordinate. To get the top left side of the box we divide our loop values by 10. To get the bottom left side of the box we divide our loop values by 10 and add 0.1f to the y texture coordinate. Finally to get the bottom right texture coordinate we divide the loop values by 10 and add 0.1f to both the x and y texture coordinates.

    Quick examples: loop1=0 and loop2=0


    Right X Texture Coordinate = loop1/10+0.1f = 0/10+0.1f = 0+0.1f = 0.1f
    Left X Texture Coordinate = loop1/10 = 0/10 = 0.0f
    Top Y Texture Coordinate = loop2/10 = 0/10 = 0.0f;
    Bottom Y Texture Coordinate = loop2/10+0.1f = 0/10+0.1f = 0+0.1f = 0.1f;

    loop1=1 and loop2=1


    Right X Texture Coordinate = loop1/10+0.1f = 1/10+0.1f = 0.1f+0.1f = 0.2f
    Left X Texture Coordinate = loop1/10 = 1/10 = 0.1f
    Top Y Texture Coordinate = loop2/10 = 1/10 = 0.1f;
    Bottom Y Texture Coordinate = loop2/10+0.1f = 1/10+0.1f = 0.1f+0.1f = 0.2f;

    Hopefully that all makes sense. If loop1 and loop2 were equal to 9 we would end up with the values 0.9f and 1.0f. So as you can see our texture coordinates mapped across the 10 boxes run from 0.0f at the lowest and 1.0f at the highest. Mapping the entire texture to the screen. After we've mapped a section of the texture to the screen, we disable texture mapping. Once we've drawn all the lines and filled in all the boxes, we set the line width to 1.0f.
      
       

       glEnable(GL_TEXTURE_2D);    // Enable Texture Mapping
       glColor3f(1.0f,1.0f,1.0f);    // Bright White Color
       glBindTexture(GL_TEXTURE_2D, texture[1]);  // Select The Tile Image
       if ((loop1<10) && (loop2<10))    // If In Bounds, Fill In Traced Boxes
       {
        // Are All Sides Of The Box Traced?
        if (hline[loop1][loop2] && hline[loop1][loop2+1] && vline[loop1][loop2] && vline[loop1+1][loop2])
        {
         glBegin(GL_QUADS);   // Draw A Textured Quad
          glTexCoord2f(float(loop1/10.0f)+0.1f,1.0f-(float(loop2/10.0f)));
          glVertex2d(20+(loop1*60)+59,(70+loop2*40+1)); // Top Right
          glTexCoord2f(float(loop1/10.0f),1.0f-(float(loop2/10.0f)));
          glVertex2d(20+(loop1*60)+1,(70+loop2*40+1)); // Top Left
          glTexCoord2f(float(loop1/10.0f),1.0f-(float(loop2/10.0f)+0.1f));
          glVertex2d(20+(loop1*60)+1,(70+loop2*40)+39); // Bottom Left
          glTexCoord2f(float(loop1/10.0f)+0.1f,1.0f-(float(loop2/10.0f)+0.1f));
          glVertex2d(20+(loop1*60)+59,(70+loop2*40)+39); // Bottom Right
         glEnd();    // Done Texturing The Box
        }
       }
       glDisable(GL_TEXTURE_2D);    // Disable Texture Mapping
      }
     }
     glLineWidth(1.0f);       // Set The Line Width To 1.0f

       
    The code below checks to see if anti is TRUE. If it is, we enable line smoothing (anti-aliasing).   
       

     if (anti)        // Is Anti TRUE?
     {
      glEnable(GL_LINE_SMOOTH);     // If So, Enable Antialiasing
     }

       
    To make the game a little easier I've added a special item. The item is an hourglass. When you touch the hourglass, the enemies are frozen for a specific amount of time. The following section of code is resposible for drawing the hourglass.

    For the hourglass we use x and y to position the timer, but unlike our player and enemies we don't use fx and fy for fine positioning. Instead we'll use fx to keep track of whether or not the timer is being displayed. fx will equal 0 if the timer is not visible. 1 if it is visible, and 2 if the player has touched the timer. fy will be used as a counter to keep track of how long the timer should be visible or invisible.

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

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

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给一分之千发送一个短消息 把一分之千加入好友 查看一分之千的个人资料 搜索一分之千在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看一分之千的博客3
    发贴心情 
    lesson21  (continue)
    So we start off by checking to see if the timer is visible. If not, we skip over the code without drawing the timer. If the timer is visible, we reset the modelview matrix, and position the timer. Because our first grid point from left to right starts at 20, we will add hourglass.x times 60 to 20. We multiply hourglass.x by 60 because the points on our grid from left to right are spaced 60 pixels apart. We then position the hourglass on the y axis. We add hourglass.y times 40 to 70.0f because we want to start drawing 70 pixels down from the top of the screen. Each point on our grid from top to bottom is spaced 40 pixels apart.

    After we have positioned the hourglass, we can rotate it on the z-axis. hourglass.spin is used to keep track of the rotation, the same way player.spin keeps track of the player rotation. Before we start to draw the hourglass we select a random color.   
       

     if (hourglass.fx==1)       // If fx=1 Draw The Hourglass
     {
      glLoadIdentity();      // Reset The Modelview Matrix
      glTranslatef(20.0f+(hourglass.x*60),70.0f+(hourglass.y*40),0.0f); // Move To The Fine Hourglass Position
      glRotatef(hourglass.spin,0.0f,0.0f,1.0f);   // Rotate Clockwise
      glColor3ub(rand()%255,rand()%255,rand()%255);   // Set Hourglass Color To Random Color

       
    glBegin(GL_LINES) tells OpenGL we want to draw using lines. We start off by moving left and up 5 pixels from our current location. This gives us the top left point of our hourglass. OpenGL will start drawing the line from this location. The end of the line will be 5 pixels right and down from our original location. This gives us a line running from the top left to the bottom right. Immediately after that we draw a second line running from the top right to the bottom left. This gives us an 'X'. We finish off by connecting the bottom two points together, and then the top two points to create an hourglass type object :)   
       

      glBegin(GL_LINES);      // Start Drawing Our Hourglass Using Lines
       glVertex2d(-5,-5);     // Top Left Of Hourglass
       glVertex2d( 5, 5);     // Bottom Right Of Hourglass
       glVertex2d( 5,-5);     // Top Right Of Hourglass
       glVertex2d(-5, 5);     // Bottom Left Of Hourglass
       glVertex2d(-5, 5);     // Bottom Left Of Hourglass
       glVertex2d( 5, 5);     // Bottom Right Of Hourglass
       glVertex2d(-5,-5);     // Top Left Of Hourglass
       glVertex2d( 5,-5);     // Top Right Of Hourglass
      glEnd();       // Done Drawing The Hourglass
     }

       
    Now we draw our player. We reset the modelview matrix, and position the player on the screen. Notice we position the player using fx and fy. We want the player to move smoothly so we use fine positioning. After positioning the player, we rotate the player on it's z-axis using player.spin. We set the color to light green and begin drawing. Just like the code we used to draw the hourglass, we draw an 'X'. Starting at the top left to the bottom right, then from the top right to the bottom left.   
       

     glLoadIdentity();       // Reset The Modelview Matrix
     glTranslatef(player.fx+20.0f,player.fy+70.0f,0.0f);   // Move To The Fine Player Position
     glRotatef(player.spin,0.0f,0.0f,1.0f);     // Rotate Clockwise
     glColor3f(0.0f,1.0f,0.0f);      // Set Player Color To Light Green
     glBegin(GL_LINES);       // Start Drawing Our Player Using Lines
      glVertex2d(-5,-5);      // Top Left Of Player
      glVertex2d( 5, 5);      // Bottom Right Of Player
      glVertex2d( 5,-5);      // Top Right Of Player
      glVertex2d(-5, 5);      // Bottom Left Of Player
     glEnd();        // Done Drawing The Player

       
    Drawing low detail objects with lines can be a little frustrating. I didn't want the player to look boring so I added the next section of code to create a larger and quicker spinning blade on top of the player that we drew above. We rotate on the z-axis by player.spin times 0.5f. Because we are rotating again, it will appear as if this piece of the player is moving a little quicker than the first piece of the player.

    After doing the new rotation, we set the color to a darker shade of green. So that it actually looks like the player is made up of different colors / pieces. We then draw a large '+' on top of the first piece of the player. It's larger because we're using -7 and +7 instead of -5 and +5. Also notice that instead of drawing from one corner to another, I'm drawing this piece of the player from left to right and top to bottom.   
       

     glRotatef(player.spin*0.5f,0.0f,0.0f,1.0f);    // Rotate Clockwise
     glColor3f(0.0f,0.75f,0.0f);      // Set Player Color To Dark Green
     glBegin(GL_LINES);       // Start Drawing Our Player Using Lines
      glVertex2d(-7, 0);      // Left Center Of Player
      glVertex2d( 7, 0);      // Right Center Of Player
      glVertex2d( 0,-7);      // Top Center Of Player
      glVertex2d( 0, 7);      // Bottom Center Of Player
     glEnd();        // Done Drawing The Player

       
    All we have to do now is draw the enemies, and we're done drawing :) We start off by creating a loop that will loop through all the enemies visible on the current level. We calculate how many enemies to draw by multiplying our current game stage by the games internal level. Remember that each level has 3 stages, and the maximum value of the internal level is 3. So we can have a maximum of 9 enemies.

    Inside the loop we reset the modelview matrix, and position the current enemy (enemy[loop1]). We position the enemy using it's fine x and y values (fx and fy). After positioning the current enemy we set the color to pink and start drawing.

    The first line will run from 0, -7 (7 pixels up from the starting location) to -7,0 (7 pixels left of the starting location). The second line runs from -7,0 to 0,7 (7 pixels down from the starting location). The third line runs from 0,7 to 7,0 (7 pixels to the right of our starting location), and the last line runs from 7,0 back to the beginning of the first line (7 pixels up from the starting location). This creates a non spinning pink diamond on the screen.   
       

     for (loop1=0; loop1<(stage*level); loop1++)    // Loop To Draw Enemies
     {
      glLoadIdentity();      // Reset The Modelview Matrix
      glTranslatef(enemy[loop1].fx+20.0f,enemy[loop1].fy+70.0f,0.0f);
      glColor3f(1.0f,0.5f,0.5f);     // Make Enemy Body Pink
      glBegin(GL_LINES);      // Start Drawing Enemy
       glVertex2d( 0,-7);     // Top Point Of Body
       glVertex2d(-7, 0);     // Left Point Of Body
       glVertex2d(-7, 0);     // Left Point Of Body
       glVertex2d( 0, 7);     // Bottom Point Of Body
       glVertex2d( 0, 7);     // Bottom Point Of Body
       glVertex2d( 7, 0);     // Right Point Of Body
       glVertex2d( 7, 0);     // Right Point Of Body
       glVertex2d( 0,-7);     // Top Point Of Body
      glEnd();       // Done Drawing Enemy Body

       
    We don't want the enemy to look boring either so we'll add a dark red spinning blade ('X') on top of the diamond that we just drew. We rotate on the z-axis by enemy[loop1].spin, and then draw the 'X'. We start at the top left and draw a line to the bottom right. Then we draw a second line from the top right to the bottom left. The two lines cross eachother creating an 'X' (or blade ... grin).   
       

      glRotatef(enemy[loop1].spin,0.0f,0.0f,1.0f);   // Rotate The Enemy Blade
      glColor3f(1.0f,0.0f,0.0f);     // Make Enemy Blade Red
      glBegin(GL_LINES);      // Start Drawing Enemy Blade
       glVertex2d(-7,-7);     // Top Left Of Enemy
       glVertex2d( 7, 7);     // Bottom Right Of Enemy
       glVertex2d(-7, 7);     // Bottom Left Of Enemy
       glVertex2d( 7,-7);     // Top Right Of Enemy
      glEnd();       // Done Drawing Enemy Blade
     }
     return TRUE;        // Everything Went OK
    }

       
    I added the KillFont() command to the end of KillGLWindow(). This makes sure the font display list is destroyed when the window is destroyed.   
       

    GLvoid KillGLWindow(GLvoid)       // Properly Kill The Window
    {
     if (fullscreen)        // Are We In Fullscreen Mode?
     {
      ChangeDisplaySettings(NULL,0);     // If So Switch Back To The Desktop
      ShowCursor(TRUE);      // Show Mouse Pointer
     }

     if (hRC)        // Do We Have A Rendering Context?
     {
      if (!wglMakeCurrent(NULL,NULL))     // Are We Able To Release The DC And RC Contexts?
      {
       MessageBox(NULL,"Release Of DC And RC Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
      }

      if (!wglDeleteContext(hRC))     // Are We Able To Delete The RC?
      {
       MessageBox(NULL,"Release Rendering Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
      }
      hRC=NULL;       // Set RC To NULL
     }

     if (hDC && !ReleaseDC(hWnd,hDC))     // Are We Able To Release The DC
     {
      MessageBox(NULL,"Release Device Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
      hDC=NULL;       // Set DC To NULL
     }

     if (hWnd && !DestroyWindow(hWnd))     // Are We Able To Destroy The Window?
     {
      MessageBox(NULL,"Could Not Release hWnd.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
      hWnd=NULL;       // Set hWnd To NULL
     }

     if (!UnregisterClass("OpenGL",hInstance))    // Are We Able To Unregister Class
     {
      MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
      hInstance=NULL;       // Set hInstance To NULL
     }

     KillFont();        // Kill The Font We Built
    }

       
    The CreateGLWindow() and WndProc() code hasn't changed so search until you find the following section of code.   
       

    int WINAPI WinMain( HINSTANCE hInstance,    // Instance
       HINSTANCE hPrevInstance,    // Previous Instance
       LPSTR  lpCmdLine,    // Command Line Parameters
       int  nCmdShow)    // Window Show State
    {
     MSG msg;        // Windows Message Structure
     BOOL done=FALSE;       // Bool Variable To Exit Loop

     // Ask The User Which Screen Mode They Prefer
     if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)
     {
      fullscreen=FALSE;      // Windowed Mode
     }

       
    This section of code hasn't changed that much. I changed the window title to read "NeHe's Line Tutorial", and I added the ResetObjects() command. This sets the player to the top left point of the grid, and gives the enemies random starting locations. The enemies will always start off at least 5 tiles away from you. TimerInit() initializes the timer so it's set up properly.   
       

     if (!CreateGLWindow("NeHe's Line Tutorial",640,480,16,fullscreen)) // Create Our OpenGL Window
     {
      return 0;       // Quit If Window Was Not Created
     }

     ResetObjects();        // Set Player / Enemy Starting Positions
     TimerInit();        // Initialize The Timer

     while(!done)        // Loop That Runs While done=FALSE
     {
      if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))   // Is There A Message Waiting?
      {
       if (msg.message==WM_QUIT)    // Have We Received A Quit Message?
       {
        done=TRUE;     // If So done=TRUE
       }
       else       // If Not, Deal With Window Messages
       {
        TranslateMessage(&msg);    // Translate The Message
        DispatchMessage(&msg);    // Dispatch The Message
       }
      }
      else        // If There Are No Messages
      {

       
    Now to make the timing code work. Notice before we draw our scene we grab the time, and store it in a floating point variable called start. We then draw the scene and swap buffers.

    Immediately after we swap the buffers we create a delay. We do this by checking to see if the current value of the timer (TimerGetTime( )) is less than our starting value plus the game stepping speed times 2. If the current timer value is less than the value we want, we endlessly loop until the current timer value is equal to or greater than the value we want. This slows down REALLY fast systems.

    Because we use the stepping speed (set by the value of adjust) the program will always run the same speed. For example, if our stepping speed was 1 we would wait until the timer was greater than or equal to 2 (1*2). But if we increased the stepping speed to 2 (causing the player to move twice as many pixels at a time), the delay is increased to 4 (2*2). So even though we are moving twice as fast, the delay is twice as long, so the game still runs the same speed :)

    One thing alot of people like to do is take the current time, and subtract the old time to find out how much time has passed. Then they move objects a certain distance based on the amount of time that has passed. Unfortunately I can't do that in this program because the fine movement has to be exact so that the player can line up with the lines on the grid. If the current fine x position was 59 and the computer decided the player needed to move two pixels, the player would never line up with the vertical line at position 60 on the grid.   
       

       float start=TimerGetTime();    // Grab Timer Value Before We Draw

       // Draw The Scene.  Watch For ESC Key And Quit Messages From DrawGLScene()
       if ((active && !DrawGLScene()) || keys[VK_ESCAPE]) // Active?  Was There A Quit Received?
       {
        done=TRUE;     // ESC or DrawGLScene Signalled A Quit
       }
       else       // Not Time To Quit, Update Screen
       {
        SwapBuffers(hDC);    // Swap Buffers (Double Buffering)
       }

       while(TimerGetTime()<start+float(steps[adjust]*2.0f)) {}// Waste Cycles On Fast Systems

       
    The following code hasn't really changed. I changed the title of the window to read "NeHe's Line Tutorial".   
       

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

       
    This section of code checks to see if the A key is being pressed and not held. If 'A' is being pressed, ap becomes TRUE (telling our program that A is being held down), and anti is toggled from TRUE to FALSE or FALSE to TRUE. Remember that anti is checked in the drawing code to see if antialiasing is turned on or off.

    If the 'A' key has been released (is FALSE) then ap is set to FALSE telling the program that the key is no longer being held down.   
       

       if (keys['A'] && !ap)     // If 'A' Key Is Pressed And Not Held
       {
        ap=TRUE;     // ap Becomes TRUE
        anti=!anti;     // Toggle Antialiasing
       }
       if (!keys['A'])      // If 'A' Key Has Been Released
       {
        ap=FALSE;     // ap Becomes FALSE
       }

       
    Now to move the enemies. I wanted to keep this section of code really simple. There is very little logic. Basically, the enemies check to see where you are and they move in that direction. Because I'm checking the actual x and y position of the players and no the fine values, the players seem to have a little more intelligence. They may see that you are way at the top of the screen. But by the time they're fine value actually gets to the top of the screen, you could already be in a different location. This causes them to sometimes move past you, before they realize you are no longer where they thought you were. May sound like they're really dumb, but because they sometimes move past you, you might find yourself being boxed in from all directions.

    We start off by checking to make sure the game isn't over, and that the window (if in windowed mode) is still active. By checking active the enemies wont move if the screen is minimized. This gives you a convenient pause feature when you need to take a break :)

    After we've made sure the enemies should be moving, we create a loop. The loop will loop through all the visible enemies. Again we calculate how many enemies should be on the screen by multiplying the current stage by the current internal level.   
       

       if (!gameover && active)    // If Game Isn't Over And Programs Active Move Objects
       {
        for (loop1=0; loop1<(stage*level); loop1++) // Loop Through The Different Stages
        {

       
    Now we move the current enemy (enemy[loop1]). We start off by checking to see if the enemy's x position is less than the players x position and we make sure that the enemy's fine y position lines up with a horizontal line. We can't move the enemy left and right if it's not on a horizontal line. If we did, the enemy would cut right through the middle of the boxes, making the game even more difficult :)

    If the enemy x position is less than the player x position, and the enemy's fine y position is lined up with a horizontal line, we move the enemy x position one block closer to the current player position.

    We also do this to move the enemy left, down and up. When moving up and down, we need to make sure the enemy's fine x position lines up with a vertical line. We don't want the enemy cutting through the top or bottom of a box.

    Note: changing the enemies x and y positions doesn't move the enemy on the screen. Remember that when we drew the enemies we used the fine positions to place the enemies on the screen. Changing the x and y positions just tells our program where we WANT the enemies to move.   
       

         if ((enemy[loop1].x<player.x) && (enemy[loop1].fy==enemy[loop1].y*40))
         {
          enemy[loop1].x++;  // Move The Enemy Right
         }

         if ((enemy[loop1].x>player.x) && (enemy[loop1].fy==enemy[loop1].y*40))
         {
          enemy[loop1].x--;  // Move The Enemy Left
         }

         if ((enemy[loop1].y<player.y) && (enemy[loop1].fx==enemy[loop1].x*60))
         {
          enemy[loop1].y++;  // Move The Enemy Down
         }

         if ((enemy[loop1].y>player.y) && (enemy[loop1].fx==enemy[loop1].x*60))
         {
          enemy[loop1].y--;  // Move The Enemy Up
         }

       
    This code does the actual moving. We check to see if the variable delay is greater than 3 minus the current internal level. That way if our current level is 1 the program will loop through 2 (3-1) times before the enemies actually move. On level 3 (the highest value that level can be) the enemies will move the same speed as the player (no delays). We also make sure that hourglass.fx isn't the same as 2. Remember, if hourglass.fx is equal to 2, that means the player has touched the hourglass. Meaning the enemies shouldn't be moving.

    If delay is greater than 3-level and the player hasn't touched the hourglass, we move the enemies by adjusting the enemy fine positions (fx and fy). The first thing we do is set delay back to 0 so that we can start the delay counter again. Then we set up a loop that loops through all the visible enemies (stage times level).   
       

         if (delay>(3-level) && (hourglass.fx!=2))  // If Our Delay Is Done And Player Doesn't Have Hourglass
         {
          delay=0;     // Reset The Delay Counter Back To Zero
          for (loop2=0; loop2<(stage*level); loop2++) // Loop Through All The Enemies
          {

       
    To move the enemies we check to see if the current enemy (enemy[loop2]) needs to move in a specific direction to move towards the enemy x and y position we want. In the first line below we check to see if the enemy fine position on the x-axis is less than the desired x position times 60. (remember each grid crossing is 60 pixels apart from left to right). If the fine x position is less than the enemy x position times 60 we move the enemy to the right by steps[adjust] (the speed our game is set to play at based on the value of adjust). We also rotate the enemy clockwise to make it look like it's rolling to the right. We do this by increasing enemy[loop2].spin by steps[adjust] (the current game speed based on adjust).

    We then check to see if the enemy fx value is greater than the enemy x position times 60 and if so, we move the enemy left and spin the enemy left.

    We do the same when moving the enemy up and down. If the enemy y position is less than the enemy fy position times 40 (40 pixels between grid points up and down) we increase the enemy fy position, and rotate the enemy to make it look like it's rolling downwards. Lastly if the enemy y position is greater than the enemy fy position times 40 we decrease the value of fy to move the enemy upward. Again, the enemy spins to make it look like it's rolling upward.   
       

           if (enemy[loop2].fx<enemy[loop2].x*60) // Is Fine Position On X Axis Lower Than Intended Position?
           {
            enemy[loop2].fx+=steps[adjust]; // If So, Increase Fine Position On X Axis
            enemy[loop2].spin+=steps[adjust]; // Spin Enemy Clockwise
           }
           if (enemy[loop2].fx>enemy[loop2].x*60) // Is Fine Position On X Axis Higher Than Intended Position?
           {
            enemy[loop2].fx-=steps[adjust]; // If So, Decrease Fine Position On X Axis
            enemy[loop2].spin-=steps[adjust]; // Spin Enemy Counter Clockwise
           }
           if (enemy[loop2].fy<enemy[loop2].y*40) // Is Fine Position On Y Axis Lower Than Intended Position?
           {
            enemy[loop2].fy+=steps[adjust]; // If So, Increase Fine Position On Y Axis
            enemy[loop2].spin+=steps[adjust]; // Spin Enemy Clockwise
           }
           if (enemy[loop2].fy>enemy[loop2].y*40) // Is Fine Position On Y Axis Higher Than Intended Position?
           {
            enemy[loop2].fy-=steps[adjust]; // If So, Decrease Fine Position On Y Axis
            enemy[loop2].spin-=steps[adjust]; // Spin Enemy Counter Clockwise
           }
          }
         }

       
    After moving the enemies we check to see if any of them have hit the player. We want accuracy so we compare the enemy fine positions with the player fine positions. If the enemy fx position equals the player fx position and the enemy fy position equals the player fy position the player is DEAD :)

    If the player is dead, we decrease lives. Then we check to make sure the player isn't out of lives by checking to see if lives equals 0. If lives does equal zero, we set gameover to TRUE.

    We then reset our objects by calling ResetObjects(), and play the death sound.

    Sound is new in this tutorial. I've decided to use the most basic sound routine available... PlaySound(). PlaySound() takes three parameters. First we give it the name of the file we want to play. In this case we want it to play the Die .WAV file in the Data directory. The second parameter can be ignored. We'll set it to NULL. The third parameter is the flag for playing the sound. The two most common flags are: SND_SYNC which stops everything else until the sound is done playing, and SND_ASYNC, which plays the sound, but doesn't stop the program from running. We want a little delay after the player dies so we use SND_SYNC. Pretty easy!

    The one thing I forgot to mention at the beginning of the program: In order for PlaySound() and the timer to work, you have to include the WINMM.LIB file under PROJECT / SETTINGS / LINK in Visual C++. Winmm.lib is the Windows Multimedia Library. If you don't include this library, you will get error messages when you try to compile the program.   
       

         // Are Any Of The Enemies On Top Of The Player?
         if ((enemy[loop1].fx==player.fx) && (enemy[loop1].fy==player.fy))
         {
          lives--;   // If So, Player Loses A Life

          if (lives==0)   // Are We Out Of Lives?
          {
           gameover=TRUE;  // If So, gameover Becomes TRUE
          }

          ResetObjects();   // Reset Player / Enemy Positions
          PlaySound("Data/Die.wav", NULL, SND_SYNC); // Play The Death Sound
         }
        }

       
    Now we can move the player. In the first line of code below we check to see if the right arrow is being pressed, player.x is less than 10 (don't want to go off the grid), that player.fx equals player.x times 60 (lined up with a grid crossing on the x-axis, and that player.fy equals player.y times 40 (player is lined up with a grid crossing on the y-axis).

    If we didn't make sure the player was at a crossing, and we allowed the player to move anyways, the player would cut right through the middle of boxes, just like the enemies would have done if we didn't make sure they were lined up with a vertical or horizontal line. Checking this also makes sure the player is done moving before we move to a new location.

    If the player is at a grid crossing (where a vertical and horizontal lines meet) and he's not to far right, we mark the current horizontal line that we are on as being traced over. We then increase the player.x value by one, causing the new player position to be one box to the right.

    We do the same thing while moving left, down and up. When moving left, we make sure the player wont be going off the left side of the grid. When moving down we make sure the player wont be leaving the bottom of the grid, and when moving up we make sure the player doesn't go off the top of the grid.

    When moving left and right we make the horizontal line (hline[ ] [ ]) under us TRUE meaning it's been traced. When moving up and down we make the vertical line (vline[ ] [ ]) under us TRUE meaning it has been traced.   
       

        if (keys[VK_RIGHT] && (player.x<10) && (player.fx==player.x*60) && (player.fy==player.y*40))
        {
         hline[player.x][player.y]=TRUE;  // Mark The Current Horizontal Border As Filled
         player.x++;    // Move The Player Right
        }
        if (keys[VK_LEFT] && (player.x>0) && (player.fx==player.x*60) && (player.fy==player.y*40))
        {
         player.x--;    // Move The Player Left
         hline[player.x][player.y]=TRUE;  // Mark The Current Horizontal Border As Filled
        }
        if (keys[VK_DOWN] && (player.y<10) && (player.fx==player.x*60) && (player.fy==player.y*40))
        {
         vline[player.x][player.y]=TRUE;  // Mark The Current Verticle Border As Filled
         player.y++;    // Move The Player Down
        }
        if (keys[VK_UP] && (player.y>0) && (player.fx==player.x*60) && (player.fy==player.y*40))
        {
         player.y--;    // Move The Player Up
         vline[player.x][player.y]=TRUE;  // Mark The Current Verticle Border As Filled
        }

       
    We increase / decrease the player fine fx and fy variables the same way we increase / decreased the enemy fine fx and fy variables.

    If the player fx value is less than the player x value times 60 we increase the player fx position by the step speed our game is running at based on the value of adjust.

    If the player fx value is greater than the player x value times 60 we decrease the player fx position by the step speed our game is running at based on the value of adjust.

    If the player fy value is less than the player y value times 40 we increase the player fy position by the step speed our game is running at based on the value of adjust.

    If the player fy value is greater than the player y value times 40 we decrease the player fy position by the step speed our game is running at based on the value of adjust.   
       

        if (player.fx<player.x*60)   // Is Fine Position On X Axis Lower Than Intended Position?
        {
         player.fx+=steps[adjust];  // If So, Increase The Fine X Position
        }
        if (player.fx>player.x*60)   // Is Fine Position On X Axis Greater Than Intended Position?
        {
         player.fx-=steps[adjust];  // If So, Decrease The Fine X Position
        }
        if (player.fy<player.y*40)   // Is Fine Position On Y Axis Lower Than Intended Position?
        {
         player.fy+=steps[adjust];  // If So, Increase The Fine Y Position
        }
        if (player.fy>player.y*40)   // Is Fine Position On Y Axis Lower Than Intended Position?
        {
         player.fy-=steps[adjust];  // If So, Decrease The Fine Y Position
        }
       }

       
    If the game is over the following bit of code will run. We check to see if the spacebar is being pressed. If it is we set gameover to FALSE (starting the game over). We set filled to TRUE. This causes the game to think we've finished a stage, causing the player to be reset, along with the enemies.

    We set the starting level to 1, along with the actual displayed level (level2). We set stage to 0. The reason we do this is because after the computer sees that the grid has been filled in, it will think you finished a stage, and will increase stage by 1. Because we set stage to 0, when the stage increases it will become 1 (exactly what we want). Lastly we set lives back to 5.   
       

       else       // Otherwise
       {
        if (keys[' '])     // If Spacebar Is Being Pressed
        {
         gameover=FALSE;    // gameover Becomes FALSE
         filled=TRUE;    // filled Becomes TRUE
         level=1;    // Starting Level Is Set Back To One
         level2=1;    // Displayed Level Is Also Set To One
         stage=0;    // Game Stage Is Set To Zero
         lives=5;    // Lives Is Set To Five
        }
       }

       
    The code below checks to see if the filled flag is TRUE (meaning the grid has been filled in). filled can be set to TRUE one of two ways. Either the grid is filled in completely and filled becomes TRUE or the game has ended but the spacebar was pressed to restart it (code above).

    If filled is TRUE, the first thing we do is play the cool level complete tune. I've already explained how PlaySound() works. This time we'll be playing the Complete .WAV file in the DATA directory. Again, we use SND_SYNC so that there is a delay before the game starts on the next stage.

    After the sound has played, we increase stage by one, and check to make sure stage isn't greater than 3. If stage is greater than 3 we set stage to 1, and increase the internal level and visible level by one.

    If the internal level is greater than 3 we set the internal leve (level) to 3, and increase lives by 1. If you're amazing enough to get past level 3 you deserve a free life :). After increasing lives we check to make sure the player doesn't have more than 5 lives. If lives is greater than 5 we set lives back to 5.   
       

       if (filled)      // Is The Grid Filled In?
       {
        PlaySound("Data/Complete.wav", NULL, SND_SYNC); // If So, Play The Level Complete Sound
        stage++;     // Increase The Stage
        if (stage>3)     // Is The Stage Higher Than 3?
        {
         stage=1;    // If So, Set The Stage To One
         level++;    // Increase The Level
         level2++;    // Increase The Displayed Level
         if (level>3)    // Is The Level Greater Than 3?
         {
          level=3;   // If So, Set The Level To 3
          lives++;   // Give The Player A Free Life
          if (lives>5)   // Does The Player Have More Than 5 Lives?
          {
           lives=5;  // If So, Set Lives To Five
          }
         }
        }

       
    We then reset all the objects (such as the player and enemies). This places the player back at the top left corner of the grid, and gives the enemies random locations on the grid.

    We create two loops (loop1 and loop2) to loop through the grid. We set all the vertical and horizontal lines to FALSE. If we didn't do this, the next stage would start, and the game would think the grid was still filled in.

    Notice the routine we use to clear the grid is similar to the routine we use to draw the grid. We have to make sure the lines are not being drawn to far right or down. That's why we check to make sure that loop1 is less than 10 before we reset the horizontal lines, and we check to make sure that loop2 is less than 10 before we reset the vertical lines.   
       

        ResetObjects();     // Reset Player / Enemy Positions

        for (loop1=0; loop1<11; loop1++)  // Loop Through The Grid X Coordinates
        {
         for (loop2=0; loop2<11; loop2++) // Loop Through The Grid Y Coordinates
         {
          if (loop1<10)   // If X Coordinate Is Less Than 10
          {
           hline[loop1][loop2]=FALSE; // Set The Current Horizontal Value To FALSE
          }
          if (loop2<10)   // If Y Coordinate Is Less Than 10
          {
           vline[loop1][loop2]=FALSE; // Set The Current Vertical Value To FALSE
          }
         }
        }
       }

       
    Now we check to see if the player has hit the hourglass. If the fine player fx value is equal to the hourglass x value times 60 and the fine player fy value is equal to the hourglass y value times 40 AND hourglass.fx is equal to 1 (meaning the hourglass is displayed on the screen), the code below runs.

    The first line of code is PlaySound("Data/freeze.wav",NULL, SND_ASYNC | SND_LOOP). This line plays the freeze .WAV file in the DATA directory. Notice we are using SND_ASYNC this time. We want the freeze sound to play without the game stopping. SND_LOOP keeps the sound playing endlessly until we tell it to stop playing, or until another sound is played.

    After we have started the sound playing, we set hourglass.fx to 2. When hourglass.fx equals 2 the hourglass will no longer be drawn, the enemies will stop moving, and the sound will loop endlessly.

    We also set hourglass.fy to 0. hourglass.fy is a counter. When it hits a certain value, the value of hourglass.fx will change.   
       

       // If The Player Hits The Hourglass While It's Being Displayed On The Screen
       if ((player.fx==hourglass.x*60) && (player.fy==hourglass.y*40) && (hourglass.fx==1))
       {
        // Play Freeze Enemy Sound
        PlaySound("Data/freeze.wav", NULL, SND_ASYNC | SND_LOOP);
        hourglass.fx=2;     // Set The hourglass fx Variable To Two
        hourglass.fy=0;     // Set The hourglass fy Variable To Zero
       }

       
    This bit of code increases the player spin value by half the speed that the game runs at. If player.spin is greater than 360.0f we subtract 360.0f from player.spin. Keeps the value of player.spin from getting to high.   
       

       player.spin+=0.5f*steps[adjust];   // Spin The Player Clockwise
       if (player.spin>360.0f)     // Is The spin Value Greater Than 360?
       {
        player.spin-=360;    // If So, Subtract 360
       }

       
    The code below decreases the hourglass spin value by 1/4 the speed that the game is running at. If hourglass.spin is less than 0.0f we add 360.0f. We don't want hourglass.spin to become a negative number.   
       

       hourglass.spin-=0.25f*steps[adjust];   // Spin The Hourglass Counter Clockwise
       if (hourglass.spin<0.0f)    // Is The spin Value Less Than 0?
       {
        hourglass.spin+=360.0f;    // If So, Add 360
       }

       
    The first line below increased the hourglass counter that I was talking about. hourglass.fy is increased by the game speed (game speed is the steps value based on the value of adjust).

    The second line checks to see if hourglass.fx is equal to 0 (non visible) and the hourglass counter (hourglass.fy) is greater than 6000 divided by the current internal level (level).

    If the fx value is 0 and the counter is greater than 6000 divided by the internal level we play the hourglass .WAV file in the DATA directory. We don't want the action to stop so we use SND_ASYNC. We won't loop the sound this time though, so once the sound has played, it wont play again.

    After we've played the sound we give the hourglass a random value on the x-axis. We add one to the random value so that the hourglass doesn't appear at the players starting position at the top left of the grid. We also give the hourglass a random value on the y-axis. We set hourglass.fx to 1 this makes the hourglass appear on the screen at it's new location. We also set hourglass.fy back to zero so it can start counting again.

    This causes the hourglass to appear on the screen after a fixed amount of time.   
       

       hourglass.fy+=steps[adjust];    // Increase The hourglass fy Variable
       if ((hourglass.fx==0) && (hourglass.fy>6000/level)) // Is The hourglass fx Variable Equal To 0 And The fy
       {       // Variable Greater Than 6000 Divided By The Current Level?
        PlaySound("Data/hourglass.wav", NULL, SND_ASYNC); // If So, Play The Hourglass Appears Sound
        hourglass.x=rand()%10+1;   // Give The Hourglass A Random X Value
        hourglass.y=rand()%11;    // Give The Hourglass A Random Y Value
        hourglass.fx=1;     // Set hourglass fx Variable To One (Hourglass Stage)
        hourglass.fy=0;     // Set hourglass fy Variable To Zero (Counter)
       }

       
    If hourglass.fx is equal to zero and hourglass.fy is greater than 6000 divided by the current internal level (level) we set hourglass.fx back to 0, causing the hourglass to disappear. We also set hourglass.fy to 0 so it can start counting once again.

    This causes the hourglass to disappear if you don't get it after a certain amount of time.   
       

       if ((hourglass.fx==1) && (hourglass.fy>6000/level)) // Is The hourglass fx Variable Equal To 1 And The fy
       {       // Variable Greater Than 6000 Divided By The Current Level?
        hourglass.fx=0;     // If So, Set fx To Zero (Hourglass Will Vanish)
        hourglass.fy=0;     // Set fy to Zero (Counter Is Reset)
       }

       
    Now we check to see if the 'freeze enemy' timer has run out after the player has touched the hourglass.

    if hourglass.fx equal 2 and hourglass.fy is greater than 500 plus 500 times the current internal level we kill the timer sound that we started playing endlessly. We kill the sound with the command PlaySound(NULL, NULL, 0). We set hourglass.fx back to 0, and set hourglass.fy to 0. Setting fx and fy to 0 starts the hourglass cycle from the beginning. fy will have to hit 6000 divided by the current internal level before the hourglass appears again.   
       

       if ((hourglass.fx==2) && (hourglass.fy>500+(500*level)))// Is The hourglass fx Variable Equal To 2 And The fy
       {       // Variable Greater Than 500 Plus 500 Times The Current Level?
        PlaySound(NULL, NULL, 0);   // If So, Kill The Freeze Sound
        hourglass.fx=0;     // Set hourglass fx Variable To Zero
        hourglass.fy=0;     // Set hourglass fy Variable To Zero
       }

       
    The last thing to do is increase the variable delay. If you remember, delay is used to update the player movement and animation. If our program has finished, we kill the window and return to the desktop.   
       

       delay++;      // Increase The Enemy Delay Counter
      }
     }

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

       
    I spent a long time writing this tutorial. It started out as a simple line tutorial, and flourished into an entertaining mini game. Hopefully you can use what you have learned in this tutorial in GL projects of your own. I know alot of you have been asking about TILE based games. Well you can't get more tiled than this :) I've also gotten alot of emails asking how to do exact pixel plotting. I think I've got it covered :) Most importantly, this tutorial not only teaches you new things about OpenGL, it also teaches you how to use simple sounds to add excitement to your visual works of art! I hope you've enjoyed this tutorial. If you feel I have incorrectly commented something or that the code could be done better in some sections, please let me know. I want to make the best OpenGL tutorials I can and I'm interested in hearing your feedback.

    Please note, this was an extremely large projects. I tried to comment everything as clearly as possible, but putting what things into words isn't as easy as it may seem. I know how everything works off by heart, but trying to explain is a different story :) If you've read through the tutorial and have a better way to word things, or if you feel diagrams might help out, please send me suggestions. I want this tutorial to be easy to follow through. Also note that this is not a beginner tutorial. If you haven't read through the previous tutorials please don't email me with questions until you have. Thanks.

    Jeff Molofee (NeHe)

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

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

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

    第二十二课

    按此在新窗口浏览图片凹凸映射,多重纹理扩展:

    这是一课高级教程,请确信你对基本知识已经非常了解了。这一课是基于第六课的代码的,它将建立一个非常酷的立体纹理效果。

      
       
       
    这一课由Jens Schneider所写,它基本上是由第6课改写而来的,在这一课里,你将学习:
    怎样控制多重纹理
    怎样创建一个“假”的凹凸映射
    怎样做一个标志,它看起来在你的场景上方
    怎样使矩阵变化更有效率
    基本的多通道渲染
    因为上面提到的很多方面是高级渲染得内容,我们在讲述的时候会先说明理论,接着在分析代码 。如果你已经熟悉了这些理论,你可以跳过他们,直接看代码。当你遇到什么问题的时候,不妨回过头来看看这些理论。
    最后这份代码超过了1200行,大部分我们在前面的教程中遇到过了。我不会解释每一行代码,只在重要的地方做些提示,好了,让我们开始吧。  
       

    #include <string.h>       // 字符串处理函数
       
    MAX_EMBOSS常量定义了突起的最大值  
       

    #define MAX_EMBOSS (GLfloat)0.01f      // 定义了突起的最大值

       
    好了,现在我们准备使用GL_ARB_multitexture这个扩展,它非常简单。
    大部分图形卡不止一个纹理单元,为了利用这个功能,你必须检查GL_ARB_multitexture是否被支持,它可以使你同时把2个或多个不同的纹理映射到OpenGL图元上。开起来这个功能好像没有太大的作用,但当你使用多个纹理时,如果能同时把这些纹理值混合,而不使用费时的乘法运算,你将会得到很高的速度提高。

    现在回到我们的代码,__ARB_ENABLE用来设置是否使用ARB扩展。如果你想看你的OpenGL扩展,只要把#define EXT_INFO前的注释去掉就行了。接着,我们在运行检查我们的扩展,以保证我们的程序可以在不同的系统上运行。所以我们需要一些内存保存扩展名的字符串,他们是下面两行。接着我们用一个变量multitextureSupported来标志当前系统是否能使用multitexture扩展,并用maxTexelUnits记录运行系统的纹理单元,这个值最少是1。
      
       

    #define __ARB_ENABLE true       // 使用它设置是否使用ARB扩展
    // #define EXT_INFO       // 把注释去掉,可以在启动时查看你的扩展
    #define MAX_EXTENSION_SPACE 10240      // 保存扩展字符
    #define MAX_EXTENSION_LENGTH 256      // 每个扩展字符串最大的长度
    bool multitextureSupported=false;      // 标志是否支持多重渲染
    bool useMultitexture=true;       // 如果支持,是否使用它
    GLint maxTexelUnits=1;       // 纹理处理单元的个数

       
    下面的函数定义用来使用OpenGL的扩展函数,你可以把PFN-who-ever-reads-this看成是预先定义的函数类型,因为我们不清楚是否能得到这些函数的实体,所以先把他们都设置为NULL。glMultiTexCoordifARB函数是glTexCoordif函数的扩展,它门的功能相似,其中i为纹理坐标的维数,f为数据的类型。最后两个函数用来激活纹理处理单元,可以使用特定的纹理单元来邦定纹理。
    顺便说一句,ARB是"Architectural Review Board"的缩写,用来定义这个组织提出的对OpenGL的扩展,并不强制OpenGL的实现必须包含这个功能,但他们希望这个功能得到广泛的支持。当前,只有multitexture被加入到ARB中,这从另一个方面支持multitexture的扩展将大大的提高渲染速度。  
       

    PFNGLMULTITEXCOORD1FARBPROC glMultiTexCoord1fARB = NULL;
    PFNGLMULTITEXCOORD2FARBPROC glMultiTexCoord2fARB = NULL;
    PFNGLMULTITEXCOORD3FARBPROC glMultiTexCoord3fARB = NULL;
    PFNGLMULTITEXCOORD4FARBPROC glMultiTexCoord4fARB = NULL;
    PFNGLACTIVETEXTUREARBPROC glActiveTextureARB = NULL;
    PFNGLCLIENTACTIVETEXTUREARBPROC glClientActiveTextureARB= NULL;

       
    下面我们来定义一些全局变量:  
    filter定义过滤器类型
    texture[3]保存三个纹理
    bump[3]保存三个凹凸纹理
    invbump[3]保存三个反转了的凹凸纹理
    glLogo保存标志
    multiLogo保存多重纹理标志
      
       

    GLuint  filter=1;         // 定义过滤器类型
    GLuint  texture[3];        // 保存三个纹理
    GLuint  bump[3];         //保存三个凹凸纹理
    GLuint  invbump[3];        // 保存三个反转了的凹凸纹理
    GLuint  glLogo;         // glLogo保存标志
    GLuint  multiLogo;         // multiLogo保存多重纹理标志
    GLfloat LightAmbient[] = { 0.2f, 0.2f, 0.2f};     // 环境光
    GLfloat LightDiffuse[] = { 1.0f, 1.0f, 1.0f};     // 漫射光
    GLfloat LightPosition[] = { 0.0f, 0.0f, 2.0f};     // 光源位置
    GLfloat Gray[]  = { 0.5f, 0.5f, 0.5f, 1.0f};

       
    下面一块代码用来保存立方体的纹理和坐标,每5个数字描述一个顶点,包含2D的纹理坐标和3D的顶点坐标。  
       

    // 立方体的纹理和坐标

    GLfloat data[]= {
     // 前面
     0.0f, 0.0f,  -1.0f, -1.0f, +1.0f,
     1.0f, 0.0f,  +1.0f, -1.0f, +1.0f,
     1.0f, 1.0f,  +1.0f, +1.0f, +1.0f,
     0.0f, 1.0f,  -1.0f, +1.0f, +1.0f,
     // 背面
     1.0f, 0.0f,  -1.0f, -1.0f, -1.0f,
     1.0f, 1.0f,  -1.0f, +1.0f, -1.0f,
     0.0f, 1.0f,  +1.0f, +1.0f, -1.0f,
     0.0f, 0.0f,  +1.0f, -1.0f, -1.0f,
     // 上面
     0.0f, 1.0f,  -1.0f, +1.0f, -1.0f,
     0.0f, 0.0f,  -1.0f, +1.0f, +1.0f,
     1.0f, 0.0f,  +1.0f, +1.0f, +1.0f,
     1.0f, 1.0f,  +1.0f, +1.0f, -1.0f,
     // 下面
     1.0f, 1.0f,  -1.0f, -1.0f, -1.0f,
     0.0f, 1.0f,  +1.0f, -1.0f, -1.0f,
     0.0f, 0.0f,  +1.0f, -1.0f, +1.0f,
     1.0f, 0.0f,  -1.0f, -1.0f, +1.0f,
     // 右面
     1.0f, 0.0f,  +1.0f, -1.0f, -1.0f,
     1.0f, 1.0f,  +1.0f, +1.0f, -1.0f,
     0.0f, 1.0f,  +1.0f, +1.0f, +1.0f,
     0.0f, 0.0f,  +1.0f, -1.0f, +1.0f,
     // 左面
     0.0f, 0.0f,  -1.0f, -1.0f, -1.0f,
     1.0f, 0.0f,  -1.0f, -1.0f, +1.0f,
     1.0f, 1.0f,  -1.0f, +1.0f, +1.0f,
     0.0f, 1.0f,  -1.0f, +1.0f, -1.0f
    };

       
    下一部分代码,用来这运行时确定是否支持多重纹理的扩展。
    首先,我们假定一个字符串包含了所有的扩展名,各个扩展名之间用'\n'分开。我们所要做的就是在其中查找是否有我们需要的扩展。如果成功找到则返回TRUE,否则返回FALSE。
      
       

    bool isInString(char *string, const char *search) {
     int pos=0;
     int maxpos=strlen(search)-1;
     int len=strlen(string);
     char *other;
     for (int i=0; i<len; i++) {
      if ((i==0) || ((i>1) && string[i-1]=='\n')) {    // 新的扩展名开始与这里
       other=&string[i];
       pos=0;       // 开始新的比较
       while (string[i]!='\n') {     // 比较整个扩展名
        if (string[i]==search[pos]) pos++;   // 下一个字符
        if ((pos>maxpos) && string[i+1]=='\n') return true; // 如果整个扩展名相同则成功返回
        i++;
       }
      }
     }
     return false;        // 没找到
    }

       
    现在我们需要先取得扩展名字符串,并把它转换为以'\n'分割的字符串,接着调用以上定义的函数看看是否包含我们需要的扩展。如果定义了__ARB_ENABLE则使用多重纹理扩展,接下来我们检查是否支持GL_EXT_texture_env_combine扩展,这个扩展提供各个纹理单元复杂的交互方式,利用它可以完成复杂的混合方程。如果所有的扩展都被支持,我们首先取得纹理单元的个数,把它保存到变量maxTexelUnits中,接着通过函数wglGetProcAdress把各个函数定义连接到各自的实体上,这样在后面的程序中就可以使用这些函数了。  
       

    bool initMultitexture(void) {
     char *extensions;
     extensions=strdup((char *) glGetString(GL_EXTENSIONS));   // 返回扩展名字符串
     int len=strlen(extensions);
     for (int i=0; i<len; i++)       // 使用'\n'分割各个扩展名
      if (extensions[i]==' ') extensions[i]='\n';

    #ifdef EXT_INFO
     MessageBox(hWnd,extensions,"supported GL extensions",MB_OK | MB_ICONINFORMATION);
    #endif

     if (isInString(extensions,"GL_ARB_multitexture")    // 是否支持多重纹理扩展?
      && __ARB_ENABLE       // 是否使用多重纹理扩展?
      && isInString(extensions,"GL_EXT_texture_env_combine"))  // 是否支持纹理环境混合
     {
      glGetIntegerv(GL_MAX_TEXTURE_UNITS_ARB,&maxTexelUnits);
      glMultiTexCoord1fARB = (PFNGLMULTITEXCOORD1FARBPROC) wglGetProcAddress("glMultiTexCoord1fARB");
      glMultiTexCoord2fARB = (PFNGLMULTITEXCOORD2FARBPROC) wglGetProcAddress("glMultiTexCoord2fARB");
      glMultiTexCoord3fARB = (PFNGLMULTITEXCOORD3FARBPROC) wglGetProcAddress("glMultiTexCoord3fARB");
      glMultiTexCoord4fARB = (PFNGLMULTITEXCOORD4FARBPROC) wglGetProcAddress("glMultiTexCoord4fARB");
      glActiveTextureARB   = (PFNGLACTIVETEXTUREARBPROC) wglGetProcAddress("glActiveTextureARB");
      glClientActiveTextureARB= (PFNGLCLIENTACTIVETEXTUREARBPROC) wglGetProcAddress("glClientActiveTextureARB");

    #ifdef EXT_INFO
      MessageBox(hWnd,"The GL_ARB_multitexture 扩展被使用.","支持多重纹理",MB_OK | MB_ICONINFORMATION);
    #endif

      return true;
     }
     useMultitexture=false;       // 如果不支持多重纹理则返回false
     return false;
    }

       
    初始化灯光  
       

    void initLights(void) {
     glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient);    
     glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse);
     glLightfv(GL_LIGHT1, GL_POSITION, LightPosition);
     glEnable(GL_LIGHT1);
    }

       
    下面我们加载许多纹理,这和前面的教程很像  
       

    int LoadGLTextures() {        // 载入*.bmp图像,并转换为纹理
     bool status=true;  
     AUX_RGBImageRec *Image=NULL;      
     char *alpha=NULL;

     // 加载基础纹理
     if (Image=auxDIBImageLoad("Data/Base.bmp")) {
      glGenTextures(3, texture);      // 创建3个纹理

      // 创建使用临近过滤器过滤得纹理
      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, GL_RGB8, Image->sizeX, Image->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, Image->data);

      // 创建使用线形过滤器过滤得纹理
      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, GL_RGB8, Image->sizeX, Image->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, Image->data);

      // 创建使用线形Mipmap过滤器过滤得纹理
      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);
      gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGB8, Image->sizeX, Image->sizeY, GL_RGB, GL_UNSIGNED_BYTE, Image->data);
     }
     else status=false;

     if (Image) {        // 如果图像句柄存在,则释放图像回收资源
      if (Image->data) delete Image->data;    
      delete Image;
      Image=NULL;
     }

       
    现在我们加载凹凸映射纹理。这个纹理必须使用50%的亮度(原因我们在后面介绍),我们使用glPixelTransferf函数完成这个功能。
    另一个限制是我们不希望纹理重复贴图,只希望它粘贴一次,从纹理坐标(0,0)-(1,1),所有大于它的纹理坐标都被映射到边缘,为了完成这个功能,我们使用glTexParameteri函数。  
       

     // 载入凹凸贴图
     if (Image=auxDIBImageLoad("Data/Bump.bmp")) {
      glPixelTransferf(GL_RED_SCALE,0.5f);     // 把颜色值变为原来的50%
      glPixelTransferf(GL_GREEN_SCALE,0.5f);    
      glPixelTransferf(GL_BLUE_SCALE,0.5f);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP);  //不使用重复贴图
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP);
      glGenTextures(3, bump);      //创建凹凸贴图纹理

      // 创建使用临近过滤器过滤得纹理
      >…<
     // 创建使用线形过滤器过滤得纹理
      >…< // 创建使用线形Mipmap过滤器过滤得纹理
      >…<

       
    反转凹凸贴图数据,创建三个反转的凹凸贴图纹理  
       

      for (int i=0; i<3*Image->sizeX*Image->sizeY; i++)   // 反转凹凸贴图数据
       Image->data[i]=255-Image->data[i];

      glGenTextures(3, invbump);      // 创建三个反转了凹凸贴图

      // 创建使用临近过滤器过滤得纹理
      >…< // 创建使用线形过滤器过滤得纹理
      >…< // 创建使用线形Mipmap过滤器过滤得纹理
      >…<
     }
     else status=false;
     if (Image) {        // 如果图像存在,则删除
      if (Image->data) delete Image->data; 
      delete Image;
      Image=NULL;
     }

       
    载入标志图像,图像是把颜色和alpha通道存为两张不同的bmp位图的,所以在处理的时候需要注意以下各个分量的位置。  
       

     if (Image=auxDIBImageLoad("Data/OpenGL_ALPHA.bmp")) {
      alpha=new char[4*Image->sizeX*Image->sizeY];
      for (int a=0; a<Image->sizeX*Image->sizeY; a++)
       alpha[4*a+3]=Image->data[a*3];    
      if (!(Image=auxDIBImageLoad("Data/OpenGL.bmp"))) status=false;
      for (a=0; a<Image->sizeX*Image->sizeY; a++) {
       alpha[4*a]=Image->data[a*3];    
       alpha[4*a+1]=Image->data[a*3+1];  
       alpha[4*a+2]=Image->data[a*3+2];   
      }

      glGenTextures(1, &glLogo);      // 创建标志纹理

      // 使用线形过滤器
      glBindTexture(GL_TEXTURE_2D, glLogo);
      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, GL_RGBA8, Image->sizeX, Image->sizeY, 0, GL_RGBA, GL_UNSIGNED_BYTE, alpha);
      delete alpha;
     }
     else status=false;

     if (Image) {        // 如果图像存在,则删除
      if (Image->data) delete Image->data;    
      delete Image;
      Image=NULL;
     }

     // 载入扩展标志纹理
     if (Image=auxDIBImageLoad("Data/multi_on_alpha.bmp")) {
      alpha=new char[4*Image->sizeX*Image->sizeY];   
      >...<
      glGenTextures(1, &multiLogo);     
      >...<
      delete alpha;
     }
     else status=false;

     if (Image) {        // 如果图像存在,则删除
      if (Image->data) delete Image->data;    
      delete Image;
      Image=NULL;
     }
     return status;        
    }

       
    下面是窗口大小变化函数,没有任何改变。
    接下来是绘制一个立方体的函数,它使用常规的方法绘制。
      
       

    void doCube (void) {
     int i;
     glBegin(GL_QUADS);
      // 前面
      glNormal3f( 0.0f, 0.0f, +1.0f);
      for (i=0; i<4; i++) {
       glTexCoord2f(data[5*i],data[5*i+1]);
       glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
      }
      // 后面
      glNormal3f( 0.0f, 0.0f,-1.0f);
      for (i=4; i<8; i++) {
       glTexCoord2f(data[5*i],data[5*i+1]);
       glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
      }
      // 上面
      glNormal3f( 0.0f, 1.0f, 0.0f);
      for (i=8; i<12; i++) {
       glTexCoord2f(data[5*i],data[5*i+1]);
       glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
      }
      // 下面
      glNormal3f( 0.0f,-1.0f, 0.0f);
      for (i=12; i<16; i++) {
       glTexCoord2f(data[5*i],data[5*i+1]);
       glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
      }
      // 右面
      glNormal3f( 1.0f, 0.0f, 0.0f);
      for (i=16; i<20; i++) {
       glTexCoord2f(data[5*i],data[5*i+1]);
       glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
      }
      // 左面
      glNormal3f(-1.0f, 0.0f, 0.0f);
      for (i=20; i<24; i++) {
       glTexCoord2f(data[5*i],data[5*i+1]);
       glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
      }
     glEnd();
    }

       
    现在到了OpenGL的初始化函数,它和前面的教程基本相同,只是添加了以下代码:  
       

     multitextureSupported=initMultitexture();

     initLights(); 

       
    这里我们完成了95%的工作,下面我们来解释上面提到的原理。
    开始理论讲解(凹凸映射)
    如果你安装了Powerpoint-viewer,下面是一个讲解凹凸映射原理的PPT,你可以下载后慢慢研究:
    凹凸映射 作者Michael I. Gold, nVidia 公司
    如果你没有安装Powerpoint-viewer,我把它转换为Html格式,现讲解如下:

    凹凸贴图
    Michael I. Gold
    NVIDIA 公司

    凹凸贴图

    真实的凹凸贴图是逐像素计算的

    光照计算是按每个象素点的法向量计算的
    巨大的计算量
    更多的信息可以看: Blinn, J. Simulation of Wrinkled Surfaces. Computer Graphics. 12, 3 (August 1978), 286-292
    凹凸贴图是在效果和精度之间的一个折中

    只能对漫射光计算,不能使用反射光
    欺骗视觉的采样
    可能运行于当前的硬件上
    如果它看起来很好,就干吧
    漫射光的计算

    C = (L*N)*Dl*Dm

    L 顶点到灯之间的单位向量
    N 顶点的单位法向量
    Dl 灯光的漫射光颜色
    Dm 顶点材质的漫射属性
    凸值 逐像素改变N值
    凹凸映射 改变(L*N)的值
    近似的漫射因子 L*N

    纹理图代表高度场

    [0,1] 之间的高度代表凹凸方程
    首先导出偏离度m
    m 增加/减少基础的漫射因子Fd
    (Fd+m) 在每一像素上近似等于 (L*N)
    偏移量m的导出

    偏移量m的近似导出

    查找(s,t)纹理的高度H0
    查找(s+ds, t+dt)纹理的高度H1
    M近似等于H1-H0
    计算凹凸量

    1) 原始凸起(H0).

    2) 原始的凸起(H0)向光源移动一小段距离形成第二个凸起(H1)

    3) 用H1凸起减去H0凸起 (H1-H0)

    计算灯光亮度

    计算片断的颜色Cf

    Cf = (L*N) x Dl x Dm
    (L*N) ~ (Fd + (H1-H0))
    Ct= Dm x Dl
    Cf = (Fd + (H0-H1) x Ct
    Fd等于顶点法线与灯光的向量的乘积
    上面就是全部么? 太简单了!

    我们还没有完成所有的任务,还必须做以下内容:
    创建一个纹理
    计算纹理坐标偏移量ds, dt
    计算漫射因子Fd
    ds, dt ,Fd都从N和L导出
    现在我们开始做一些数学计算
    创建纹理

    保存纹理!
    当前的多重纹理硬件只支持两个纹理
    偏移值保存在alpha通道里
    最大凸起值为 = 1.0
    水平面值为 = 0.5
    最小值为= 0.0
    颜色存储在RGB通道中
    设置内部颜色格式为RGBA8 !!
    计算纹理偏移量

    把灯光方向向量变换到一个笛卡尔坐标系中
    顶点法线为z轴
    从法线和视口的“上”向量导出坐标系
    顶点法线为z轴
    叉乘得到X轴
    丢弃“上”向量,利用z,y轴导出x轴
    创建3x3变换矩阵Mn
    变换灯光方向向量到这个坐标系中
    计算纹理偏移量

    使用法向坐标系中的向量作为偏移量
    L'= Mn x L
    使用L’.x, L’.y 作为 ds, dt
    使用 L’.z 作为漫射因子!
    如果灯光方向接近垂直,则L’.x, L’.y 非常小
    如果灯光方向接近水平,则L’.x, L’.y 非常大
    L’.z小于零的含义?
    灯光在法线的对面
    在TNT上的实现

    计算向量,纹理坐标
    设置漫射因子
    从纹理单元0取出表面颜色和H0值
    从纹理单元1取出H1值
    ARB_multitexture 扩展
    混合纹理扩展 (TBD)

    混合0 alpha设置:
    (1-T0a) + T1a - 0.5
    T1a-T0a 映射到[-1,1],但硬件把它映射到[0,1]
    T1a为H1的值,T0a为H0的值
    0.5 平衡损失的掐除值
    使用漫射光颜色调制(相乘)片断颜色T0c
    混合1 颜色设置:
    (T0c * C0a + T0c * Fda - 0.5 )*2
    0.5 平衡损失的掐除值
    乘以2加亮图像颜色
    结束理论讲解(凹凸映射)

    虽然我们做了一些改动,使得这个程序的实现与TNT的实现不一样,但它能工作与各种不同的显卡上。在这里我们将学到两三件事,凹凸映射在大多数显卡上是一个多通道算法(在TNT系列,可以使用一个2纹理通道实现),现在你应该能想到多重纹理的好处了吧。我们将使用一个三通道非多重纹理的算法实现,这个算法可以被改写为使用一个2纹理通道实现的算法。
    现在必须告诉你,我们将要做一些矩阵和向量的乘法,但那没有什么可担心的,所有的矩阵和向量都使用齐次坐标。
      
       

    // 计算向量v=v*M(左乘)
    void VMatMult(GLfloat *M, GLfloat *v) {
     GLfloat res[3];
     res[0]=M[ 0]*v[0]+M[ 1]*v[1]+M[ 2]*v[2]+M[ 3]*v[3];
     res[1]=M[ 4]*v[0]+M[ 5]*v[1]+M[ 6]*v[2]+M[ 7]*v[3];
     res[2]=M[ 8]*v[0]+M[ 9]*v[1]+M[10]*v[2]+M[11]*v[3];
     v[0]=res[0];
     v[1]=res[1];
     v[2]=res[2];
     v[3]=M[15];        
    }

       
    开始理论讲解(凹凸映射)

    开始,让我们看看它的算法

    所有的向量必须在物体空间或则世界空间中
    计算向量v,由灯的位置减去当前顶点的位置
    归一化向量v
    把向量v投影到切空间中
    安向量v在切空间中的投影偏移纹理坐标
    这看起来不错,它基本上和Michael I. Gold介绍的方法差不多。但它有一个缺点,它只对xy平面进行投影,这对我们的应用还是不够的。
    但这个实现在计算漫射光的方法和我们是一样的,我们不能存储漫射因子,所以我们不能使用Michael I. Gold介绍的方法,因为我们想让它在任何显卡上运行而不仅仅是TNT系列。为什么不光照计算留到最后呢?这在简单的几何体绘制上是可行的,如果你需要渲染几千个具有凹凸贴图的三角形,你会感到绘制的速度不够快,到那时你需要改变这种渲染过程,寻找其它的方法。

    在我们的实现里,它看起来和上面的实现差不多,除了投影部分,我们将使用我们自己的近似。

    我们使用模型坐标,这种设定可以使得灯光位置相对于物体不变。
    我们计算当前的顶点坐标
    接着计算法线,并使它单位化
    创建一个正投影矩阵,把灯光方向变为切空间
    计算纹理坐标的偏移量,ds = s点乘v*MAX_EMBOSS, dt=t点乘v*MAX_EMBOSS
    在通道2中,把偏移量添加到纹理坐标
    为什么更好:
    更快
    看起来好看
    这个方法可以工作与各种表面
    可以运行于各种显卡
    最大化的兼容
    缺陷:
    并不是完全的物理模拟
    残留一些人为的假相

    这个示意图显示了我们坐标系统,你可以通过相减相邻的坐标来获得s,t向量,但必须保证他们构成右手系和归一化。

    结束理论讲解(凹凸映射)

    下面让我们看看如何生成偏移量,首先创建一个函数创建凹凸映射:
      
       

    // 设置纹理偏移,都为单位长度
    // n : 表面的法向量
    // c : 当前的顶点纹理坐标,返回纹理坐标的偏移量
    // l : 灯光的位置
    // s : s方向
    // t : t方向
    void SetUpBumps(GLfloat *n, GLfloat *c, GLfloat *l, GLfloat *s, GLfloat *t) {
     GLfloat v[3];        // 灯光方向
     GLfloat lenQ;        // 灯光方向向量的长度,使用它来单位化
     // 计算灯光方向
     v[0]=l[0]-c[0];
     v[1]=l[1]-c[1];
     v[2]=l[2]-c[2];
     lenQ=(GLfloat) sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);
     v[0]/=lenQ;
     v[1]/=lenQ;
     v[2]/=lenQ;
     // 把方向向量投影到s,t方向
     c[0]=(s[0]*v[0]+s[1]*v[1]+s[2]*v[2])*MAX_EMBOSS;
     c[1]=(t[0]*v[0]+t[1]*v[1]+t[2]*v[2])*MAX_EMBOSS;}
       
    那看起来复杂么,但为了理解这个效果理论是必须的。(我在写这篇教程的时候也学习了它)。
    我在程序运行的时候,总喜欢在屏幕上显示标志,现在我们有了两个,使用doLogo函数创建它。

    下面的函数显示两个标志:一个OpenGL的标志,一个多重纹理的标志,如果可以使用多重纹理,则标志使用alpha混合,并看起来半透明。为了让它在屏幕的边沿显示我们使用混合并禁用光照和深度测试。


      
       

    void doLogo(void) {
     // 必须最后在调用这个函数,以公告板的形式显示两个标志
     glDepthFunc(GL_ALWAYS);
     glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
     glEnable(GL_BLEND);
     glDisable(GL_LIGHTING);
     glLoadIdentity();
     glBindTexture(GL_TEXTURE_2D,glLogo);
     glBegin(GL_QUADS);
      glTexCoord2f(0.0f,0.0f); glVertex3f(0.23f, -0.4f,-1.0f);
      glTexCoord2f(1.0f,0.0f); glVertex3f(0.53f, -0.4f,-1.0f);
      glTexCoord2f(1.0f,1.0f); glVertex3f(0.53f, -0.25f,-1.0f);
      glTexCoord2f(0.0f,1.0f); glVertex3f(0.23f, -0.25f,-1.0f);
     glEnd();
     if (useMultitexture) {
      glBindTexture(GL_TEXTURE_2D,multiLogo);
      glBegin(GL_QUADS);
       glTexCoord2f(0.0f,0.0f); glVertex3f(-0.53f, -0.25f,-1.0f);
       glTexCoord2f(1.0f,0.0f); glVertex3f(-0.33f, -0.25f,-1.0f);
       glTexCoord2f(1.0f,1.0f); glVertex3f(-0.33f, -0.15f,-1.0f);
       glTexCoord2f(0.0f,1.0f); glVertex3f(-0.53f, -0.15f,-1.0f);
      glEnd();
     }
     glDepthFunc(GL_LEQUAL);
    }

       
    现在到了绘制凹凸贴图的函数了,我们先来看看不使用多重映射的方法,它通过三个通道实现。在第一步,我们先取得模型变换矩阵的逆矩阵!  
       

    bool doMesh1TexelUnits(void) {
     GLfloat c[4]={0.0f,0.0f,0.0f,1.0f};     // 保存当前的顶点
     GLfloat n[4]={0.0f,0.0f,0.0f,1.0f};     // 保存法线
     GLfloat s[4]={0.0f,0.0f,0.0f,1.0f};     // s纹理坐标方向
     GLfloat t[4]={0.0f,0.0f,0.0f,1.0f};     // t纹理坐标方向
     GLfloat l[4];       // 保存灯光方向
     GLfloat Minv[16];       // 保存模型变换矩阵的逆
     int i;

     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);   // 清空背景颜色和深度缓存

     // 创建模型变换矩阵的逆
     glLoadIdentity();
     glRotatef(-yrot,0.0f,1.0f,0.0f);
     glRotatef(-xrot,1.0f,0.0f,0.0f);
     glTranslatef(0.0f,0.0f,-z);
     glGetFloatv(GL_MODELVIEW_MATRIX,Minv);
     glLoadIdentity();
     glTranslatef(0.0f,0.0f,z);
     glRotatef(xrot,1.0f,0.0f,0.0f);
     glRotatef(yrot,0.0f,1.0f,0.0f);

     // 设置灯光的位置
     l[0]=LightPosition[0];
     l[1]=LightPosition[1];
     l[2]=LightPosition[2];
     l[3]=1.0f;        
     VMatMult(Minv,l);

       
    通道1:
    使用凹凸纹理
    禁止混合
    禁止光照
    使用无偏移的纹理坐标
    绘制几何体
    这将渲染一个无凹凸贴图的几何体  
       

     glBindTexture(GL_TEXTURE_2D, bump[filter]);
     glDisable(GL_BLEND);
     glDisable(GL_LIGHTING);
     doCube();

       
    通道2:
    使用反转的纹理凹凸贴图
    设置混合因子为1,1
    使用光照
    使用偏移纹理坐标
    绘制几何体
    这将绘制一个具有凹凸贴图的几何体,但没有颜色  

      
       

     glBindTexture(GL_TEXTURE_2D,invbump[filter]);
     glBlendFunc(GL_ONE,GL_ONE);
     glDepthFunc(GL_LEQUAL);
     glEnable(GL_BLEND);

     glBegin(GL_QUADS);
      // 前面
      n[0]=0.0f;
      n[1]=0.0f;
      n[2]=1.0f;
      s[0]=1.0f;
      s[1]=0.0f;
      s[2]=0.0f;
      t[0]=0.0f;
      t[1]=1.0f;
      t[2]=0.0f;
      for (i=0; i<4; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       // 设置纹理坐标为偏移后的纹理坐标
       glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // 后面
      n[0]=0.0f;
      n[1]=0.0f;
      n[2]=-1.0f;
      s[0]=-1.0f;
      s[1]=0.0f;
      s[2]=0.0f;
      t[0]=0.0f;
      t[1]=1.0f;
      t[2]=0.0f;
      for (i=4; i<8; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // 上面
      n[0]=0.0f;
      n[1]=1.0f;
      n[2]=0.0f;
      s[0]=1.0f;
      s[1]=0.0f;
      s[2]=0.0f;
      t[0]=0.0f;
      t[1]=0.0f;
      t[2]=-1.0f;
      for (i=8; i<12; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // 下面
      n[0]=0.0f;
      n[1]=-1.0f;
      n[2]=0.0f;
      s[0]=-1.0f;
      s[1]=0.0f;
      s[2]=0.0f;
      t[0]=0.0f;
      t[1]=0.0f;
      t[2]=-1.0f;
      for (i=12; i<16; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // 右面
      n[0]=1.0f;
      n[1]=0.0f;
      n[2]=0.0f;
      s[0]=0.0f;
      s[1]=0.0f;
      s[2]=-1.0f;
      t[0]=0.0f;
      t[1]=1.0f;
      t[2]=0.0f;
      for (i=16; i<20; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // 左面
      n[0]=-1.0f;
      n[1]=0.0f;
      n[2]=0.0f;
      s[0]=0.0f;
      s[1]=0.0f;
      s[2]=1.0f;
      t[0]=0.0f;
      t[1]=1.0f;
      t[2]=0.0f;
      for (i=20; i<24; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
     glEnd();

       
    通道3:
    使用颜色纹理Use (colored) base-texture
    使用混合因子GL_DST_COLOR, GL_SRC_COLOR
    这个混合等于把颜色值乘以2
    使用光照
    绘制几何体
    这个过程将结束立方体的渲染,因为我们可以在是否使用多重渲染之间切换,所以必须把纹理环境参数设为GL_MODULATE,这是默认的值。  
       

     if (!emboss) {
      glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
      glBindTexture(GL_TEXTURE_2D,texture[filter]);
      glBlendFunc(GL_DST_COLOR,GL_SRC_COLOR);
      glEnable(GL_LIGHTING);
      doCube();
     }

       
    最后的通道:
    更新几何体
    绘制标志
      
       

     xrot+=xspeed;
     yrot+=yspeed;
     if (xrot>360.0f) xrot-=360.0f;
     if (xrot<0.0f) xrot+=360.0f;
     if (yrot>360.0f) yrot-=360.0f;
     if (yrot<0.0f) yrot+=360.0f;

     //绘制标志
     doLogo();
     return true;       // 成功返回
    }

       
    这个函数将在多重纹理功能的支持下载两个通道中完成凹凸贴图的绘制,我们支持两个纹理单元,与一个纹理单元不同的是,我们给一个顶点设置两个纹理坐标。  
       

    bool doMesh2TexelUnits(void) {
     GLfloat c[4]={0.0f,0.0f,0.0f,1.0f};     // 保存当前的顶点
     GLfloat n[4]={0.0f,0.0f,0.0f,1.0f};     // 保存法线
     GLfloat s[4]={0.0f,0.0f,0.0f,1.0f};     // s纹理坐标方向
     GLfloat t[4]={0.0f,0.0f,0.0f,1.0f};     // t纹理坐标方向
     GLfloat l[4];       // 保存灯光方向
     GLfloat Minv[16];       // 保存模型变换矩阵的逆
     int i;

     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);   // 清空背景颜色和深度缓存

     // 创建模型变换矩阵的逆
     glLoadIdentity();
     glRotatef(-yrot,0.0f,1.0f,0.0f);
     glRotatef(-xrot,1.0f,0.0f,0.0f);
     glTranslatef(0.0f,0.0f,-z);
     glGetFloatv(GL_MODELVIEW_MATRIX,Minv);
     glLoadIdentity();
     glTranslatef(0.0f,0.0f,z);

     glRotatef(xrot,1.0f,0.0f,0.0f);
     glRotatef(yrot,0.0f,1.0f,0.0f);

     // 设置灯光的位置
     l[0]=LightPosition[0];
     l[1]=LightPosition[1];
     l[2]=LightPosition[2];
     l[3]=1.0f;        
     VMatMult(Minv,l);

       
    通道1:
    无凹凸贴图
    无光照
    设置纹理混合器0
    使用凹凸纹理
    使用无偏移的纹理坐标
    使用替换方式粘贴纹理
    设置纹理混合器1
    偏移纹理坐标
    使用相加的纹理操作
    这将绘制一个灰度的立方体  
       

     // 纹理单元 #0
     glActiveTextureARB(GL_TEXTURE0_ARB);
     glEnable(GL_TEXTURE_2D);
     glBindTexture(GL_TEXTURE_2D, bump[filter]);
     glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_EXT);
     glTexEnvf (GL_TEXTURE_ENV, GL_COMBINE_RGB_EXT, GL_REPLACE);

     // 纹理单元 #1
     glActiveTextureARB(GL_TEXTURE1_ARB);
     glEnable(GL_TEXTURE_2D);
     glBindTexture(GL_TEXTURE_2D, invbump[filter]);
     glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_EXT);
     glTexEnvf (GL_TEXTURE_ENV, GL_COMBINE_RGB_EXT, GL_ADD);

     // 禁用混合和光照
     glDisable(GL_BLEND);
     glDisable(GL_LIGHTING);

       
    现在按面一个一个的渲染立方体,和doMesh1TexelUnits函数中所作的操作差不多,只是用glMultiTexCoor2fARB替换glTexCoord2f,在这个函数中,你必须把纹理坐标发向不同的纹理处理单元,可用的参数值为GL_TEXTUREi_ARB0到GL_TEXTUREi_ARB31。  
       

     glBegin(GL_QUADS);
      // 前面
      n[0]=0.0f;
      n[1]=0.0f;
      n[2]=1.0f;
      s[0]=1.0f;
      s[1]=0.0f;
      s[2]=0.0f;
      t[0]=0.0f;
      t[1]=1.0f;
      t[2]=0.0f;
      for (i=0; i<4; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
       glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // 后面
      n[0]=0.0f;
      n[1]=0.0f;
      n[2]=-1.0f;
      s[0]=-1.0f;
      s[1]=0.0f;
      s[2]=0.0f;
      t[0]=0.0f;
      t[1]=1.0f;
      t[2]=0.0f;
      for (i=4; i<8; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
       glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // 上面
      n[0]=0.0f;
      n[1]=1.0f;
      n[2]=0.0f;
      s[0]=1.0f;
      s[1]=0.0f;
      s[2]=0.0f;
      t[0]=0.0f;
      t[1]=0.0f;
      t[2]=-1.0f;
      for (i=8; i<12; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
       glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // 下面
      n[0]=0.0f;
      n[1]=-1.0f;
      n[2]=0.0f;
      s[0]=-1.0f;
      s[1]=0.0f;
      s[2]=0.0f;
      t[0]=0.0f;
      t[1]=0.0f;
      t[2]=-1.0f;
      for (i=12; i<16; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
       glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // 右面
      n[0]=1.0f;
      n[1]=0.0f;
      n[2]=0.0f;
      s[0]=0.0f;
      s[1]=0.0f;
      s[2]=-1.0f;
      t[0]=0.0f;
      t[1]=1.0f;
      t[2]=0.0f;
      for (i=16; i<20; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
       glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // 左面
      n[0]=-1.0f;
      n[1]=0.0f;
      n[2]=0.0f;
      s[0]=0.0f;
      s[1]=0.0f;
      s[2]=1.0f;
      t[0]=0.0f;
      t[1]=1.0f;
      t[2]=0.0f;
      for (i=20; i<24; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
       glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
     glEnd();

       
    通道2:
    使用基本纹理
    使用光照
    使用普通的纹理混合操作
    这将完成最后的凹凸贴图  
       

     glActiveTextureARB(GL_TEXTURE1_ARB);
     glDisable(GL_TEXTURE_2D);
     glActiveTextureARB(GL_TEXTURE0_ARB);
     if (!emboss) {
      glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
      glBindTexture(GL_TEXTURE_2D,texture[filter]);
      glBlendFunc(GL_DST_COLOR,GL_SRC_COLOR);
      glEnable(GL_BLEND);
      glEnable(GL_LIGHTING);
      doCube();
     }

       
    最后的通道:
    更新几何体
    绘制标志
      
       

     xrot+=xspeed;
     yrot+=yspeed;
     if (xrot>360.0f) xrot-=360.0f;
     if (xrot<0.0f) xrot+=360.0f;
     if (yrot>360.0f) yrot-=360.0f;
     if (yrot<0.0f) yrot+=360.0f;

     doLogo();
     return true;        
    }

       
    最后绘制一个无凹凸贴图的立方体,用来观察两者之间的效果  
       

    bool doMeshNoBumps(void) {
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);   
     glLoadIdentity();       
     glTranslatef(0.0f,0.0f,z);

     glRotatef(xrot,1.0f,0.0f,0.0f);
     glRotatef(yrot,0.0f,1.0f,0.0f);

     if (useMultitexture) {
      glActiveTextureARB(GL_TEXTURE1_ARB);
      glDisable(GL_TEXTURE_2D);
      glActiveTextureARB(GL_TEXTURE0_ARB);
     }

     glDisable(GL_BLEND);
     glBindTexture(GL_TEXTURE_2D,texture[filter]);
     glBlendFunc(GL_DST_COLOR,GL_SRC_COLOR);
     glEnable(GL_LIGHTING);
     doCube();

     xrot+=xspeed;
     yrot+=yspeed;
     if (xrot>360.0f) xrot-=360.0f;
     if (xrot<0.0f) xrot+=360.0f;
     if (yrot>360.0f) yrot-=360.0f;
     if (yrot<0.0f) yrot+=360.0f;

     doLogo();
     return true;        
    }

       
    所有的绘制函数都已经完成,接下来只要在绘制函数中调用即可  
       

    bool DrawGLScene(GLvoid) 
    {
     if (bumps) {
      if (useMultitexture && maxTexelUnits>1)
       return doMesh2TexelUnits();
      else return doMesh1TexelUnits();  }
     else return doMeshNoBumps();
    }

       
    删除OpenGL窗口  
       

    GLvoid KillGLWindow(GLvoid)  
       
    创建OpenGL窗口  
       

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

       
    Windows循环  
       

    LRESULT CALLBACK WndProc( HWND hWnd,
        UINT uMsg,     
        WPARAM wParam,     
        LPARAM lParam)     
       
    在Windows主函数中加入一些控制键 :
    E: 切换凹凸贴图模式中是否带有彩色纹理
    M: 切换多重纹理模式
    B: 切换是否使用凹凸贴图
    F: 切换纹理过滤器模式
    方向键: 旋转立方体
      
       

        if (keys['E'])
        {
         keys['E']=false;
         emboss=!emboss;
        }

        if (keys['M'])
        {
         keys['M']=false;
         useMultitexture=((!useMultitexture) && multitextureSupported);
        }

        if (keys['B'])
        {
         keys['B']=false;
         bumps=!bumps;
        }

        if (keys['F'])
        {
         keys['F']=false;
         filter++;
         filter%=3;
        }

        if (keys[VK_PRIOR])
        {
         z-=0.02f;
        }

        if (keys[VK_NEXT])
        {
         z+=0.02f;
        }

        if (keys[VK_UP])
        {
         xspeed-=0.01f;
        }

        if (keys[VK_DOWN])
        {
         xspeed+=0.01f;
        }

        if (keys[VK_RIGHT])
        {
         yspeed+=0.01f;
        }

        if (keys[VK_LEFT])
        {
         yspeed-=0.01f;
        }

       
    现在你应该可以熟练的使用凹凸贴图了,如果你想让你的具有凹凸贴图的程序跑起来更快,你应该注意以下几点:
    你不应该使用256x256的纹理,这会让处理变得缓慢。
    一个具有凹凸贴图的立方体是不常见的,这和你的视角有关,因为三角面过于大了,如果要获得很好的视觉效果,你需要很大的纹理贴图,这必然会降低渲染速度。你可以把模型 创建为一些小的三角形,从而使用小的纹理,来获得好的效果。
    你应该先创建颜色纹理,接着把它转换为具有深度的凹凸纹理
    凹凸纹理应该锐化,这可以取得更好的效果,在你的图像处理程序中可以完成这个操作。
    凹凸贴图的值因该在50%灰度图上波动(RGB=127,127,127), 亮的值代表凸起,暗的值代表凹陷。
    凹凸贴图可以为纹理图大小的1/4,而不会影响外观效果。
    现在你应该对这篇文章中内容的大慨有了一个基本的认识,希望你读的愉快。
    如果你有任何纹理,请联系我或访问我的网站http://www.glhint.de
    我必须感谢以下的人:

    Michael I. Gold ,它写出了凹凸贴图的原理
    Diego Tártara ,它写出了示例代码
    NVidia 公司,他在Internet发布了大量的源码
    最后感谢Nehe,它对我的OpenGL学习起了很大的帮助

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

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

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给一分之千发送一个短消息 把一分之千加入好友 查看一分之千的个人资料 搜索一分之千在『 C/C++编程思想 』的所有贴子 引用回复这个贴子 回复这个贴子 查看一分之千的博客5
    发贴心情 
    Lesson 22
       
    This lesson was written by Jens Schneider. It is loosely based on Lesson 06, though lots of changes were made. In this lesson you will learn:
    How to control your graphic-accelerator抯 multitexture-features.
    How to do a "fake" Emboss Bump Mapping.
    How to do professional looking logos that "float" above your rendered scene using blending.
    Basics about multi-pass rendering techniques.
    How to do matrix-transformations efficiently.
    Since at least three of the above four points can be considered "advanced rendering techniques", you should already have a general understanding of OpenGL抯 rendering pipeline. You should know most commands already used in these tutorials, and you should be familiar with vector-maths. Every now and then you抣l encounter a block that reads begin theory(...) as header and end theory(...) as an ending. These sections try to teach you theory about the issue(s) mentioned in parenthesis. This is to ensure that, if you already know about the issue, you can easily skip them. If you encounter problems while trying to understand the code, consider going back to the theory sections.

    Last but not least: This lesson consists out of more than 1,200 lines of code, of which large parts are not only boring but also known among those that read earlier tutorials. Thus I will not comment each line, only the crux. If you encounter something like this >?lt;, it means that lines of code have been omitted.

    Here we go:   
       

    #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
    #include "glext.h"        // Header File For Multitexturing
    #include <string.h>        // Header File For The String Library
    #include <math.h>        // Header File For The Math Library

       
    The GLfloat MAX_EMBOSS specifies the "strength" of the Bump Mapping-Effect. Larger values strongly enhance the effect, but reduce visual quality to the same extent by leaving so-called "artefacts" at the edges of the surfaces.   
       

    #define MAX_EMBOSS (GLfloat)0.01f      // Maximum Emboss-Translate. Increase To Get Higher Immersion

       
    Ok, now let抯 prepare the use of the GL_ARB_multitexture extension. It抯 quite simple:

    Most accelerators have more than just one texture-unit nowadays. To benefit of this feature, you抣l have to check for GL_ARB_multitexture-support, which enables you to map two or more different textures to one OpenGL-primitive in just one pass. Sounds not too powerful, but it is! Nearly all the time if you抮e programming something, putting another texture on that object results in higher visual quality. Since you usually need multiple "passes" consisting out of interleaved texture-selection and drawing geometry, this can quickly become expensive. But don抰 worry, this will become clearer later on.

    Now back to code: __ARB_ENABLE is used to override multitexturing for a special compile-run entirely. If you want to see your OpenGL-extensions, just un-comment the #define EXT_INFO. Next, we want to check for our extensions during run-time to ensure our code stays portable. So we need space for some strings. These are the following two lines. Now we want to distinguish between being able to do multitexture and using it, so we need another two flags. Last, we need to know how many texture-units are present(we抮e going to use only two of them, though). At least one texture-unit is present on any OpenGL-capable accelerator, so we initialize maxTexelUnits with 1.   
       

    #define __ARB_ENABLE true       // Used To Disable ARB Extensions Entirely
    // #define EXT_INFO        // Uncomment To See Your Extensions At Start-Up?
    #define MAX_EXTENSION_SPACE 10240      // Characters For Extension-Strings
    #define MAX_EXTENSION_LENGTH 256      // Maximum Characters In One Extension-String
    bool multitextureSupported=false;      // Flag Indicating Whether Multitexturing Is Supported
    bool useMultitexture=true;       // Use It If It Is Supported?
    GLint maxTexelUnits=1;        // Number Of Texel-Pipelines. This Is At Least 1.

       
    The following lines are needed to 搇ink?the extensions to C++ function calls. Just treat the PFN-who-ever-reads-this as pre-defined datatype able to describe function calls. Since we are unsure if we抣l get the functions to these prototypes, we set them to NULL. The commands glMultiTexCoordifARB map to the well-known glTexCoordif, specifying i-dimensional texture-coordinates. Note that these can totally substitute the glTexCoordif-commands. Since we only use the GLfloat-version, we only need prototypes for the commands ending with an "f". Other are also available ("fv", "i", etc.). The last two prototypes are to set the active texture-unit that is currently receiving texture-bindings ( glActiveTextureARB() ) and to determine which texture-unit is associated with the ArrayPointer-command (a.k.a. Client-Subset, thus glClientActiveTextureARB). By the way: ARB is an abbreviation for "Architectural Review Board". Extensions with ARB in their name are not required by an OpenGL-conformant implementation, but they are expected to be widely supported. Currently, only the multitexture-extension has made it to ARB-status. This may be treated as sign for the tremendous impact regarding speed multitexturing has on several advanced rendering techniques.

    The lines ommitted are GDI-context handles etc.   
       

    PFNGLMULTITEXCOORD1FARBPROC glMultiTexCoord1fARB = NULL;
    PFNGLMULTITEXCOORD2FARBPROC glMultiTexCoord2fARB = NULL;
    PFNGLMULTITEXCOORD3FARBPROC glMultiTexCoord3fARB = NULL;
    PFNGLMULTITEXCOORD4FARBPROC glMultiTexCoord4fARB = NULL;
    PFNGLACTIVETEXTUREARBPROC glActiveTextureARB = NULL;
    PFNGLCLIENTACTIVETEXTUREARBPROC glClientActiveTextureARB= NULL;

       
    We need global variables:
    filter specifies what filter to use. Refer to Lesson 06. We抣l usually just take GL_LINEAR, so we initialise with 1.
    texture holds our base-texture, three times, one per filter.
    bump holds our bump maps
    invbump holds our inverted bump maps. This is explained later on in a theory-section.
    The Logo-things hold textures for several billboards that will be added to rendering output as a final pass.
    The Light...-stuff contains data on our OpenGL light-source.
      
       

    GLuint  filter=1;        // Which Filter To Use
    GLuint  texture[3];        // Storage For 3 Textures
    GLuint  bump[3];        // Our Bumpmappings
    GLuint  invbump[3];        // Inverted Bumpmaps
    GLuint  glLogo;         // Handle For OpenGL-Logo
    GLuint  multiLogo;        // Handle For Multitexture-Enabled-Logo
    GLfloat LightAmbient[] = { 0.2f, 0.2f, 0.2f};     // Ambient Light Is 20% White
    GLfloat LightDiffuse[] = { 1.0f, 1.0f, 1.0f};     // Diffuse Light Is White
    GLfloat LightPosition[] = { 0.0f, 0.0f, 2.0f};     // Position Is Somewhat In Front Of Screen
    GLfloat Gray[]  = { 0.5f, 0.5f, 0.5f, 1.0f};

       
    The next block of code contains the numerical representation of a textured cube built out of GL_QUADS. Each five numbers specified represent one set of 2D-texture-coordinates one set of 3D-vertex-coordinates. This is to build the cube using for-loops, since we need that cube several times. The data-block is followed by the well-known WndProc()-prototype from former lessons.   
       

    // Data Contains The Faces Of The Cube In Format 2xTexCoord, 3xVertex.
    // Note That The Tesselation Of The Cube Is Only Absolute Minimum.

    GLfloat data[]= {
     // FRONT FACE
     0.0f, 0.0f,  -1.0f, -1.0f, +1.0f,
     1.0f, 0.0f,  +1.0f, -1.0f, +1.0f,
     1.0f, 1.0f,  +1.0f, +1.0f, +1.0f,
     0.0f, 1.0f,  -1.0f, +1.0f, +1.0f,
     // BACK FACE
     1.0f, 0.0f,  -1.0f, -1.0f, -1.0f,
     1.0f, 1.0f,  -1.0f, +1.0f, -1.0f,
     0.0f, 1.0f,  +1.0f, +1.0f, -1.0f,
     0.0f, 0.0f,  +1.0f, -1.0f, -1.0f,
     // Top Face
     0.0f, 1.0f,  -1.0f, +1.0f, -1.0f,
     0.0f, 0.0f,  -1.0f, +1.0f, +1.0f,
     1.0f, 0.0f,  +1.0f, +1.0f, +1.0f,
     1.0f, 1.0f,  +1.0f, +1.0f, -1.0f,
     // Bottom Face
     1.0f, 1.0f,  -1.0f, -1.0f, -1.0f,
     0.0f, 1.0f,  +1.0f, -1.0f, -1.0f,
     0.0f, 0.0f,  +1.0f, -1.0f, +1.0f,
     1.0f, 0.0f,  -1.0f, -1.0f, +1.0f,
     // Right Face
     1.0f, 0.0f,  +1.0f, -1.0f, -1.0f,
     1.0f, 1.0f,  +1.0f, +1.0f, -1.0f,
     0.0f, 1.0f,  +1.0f, +1.0f, +1.0f,
     0.0f, 0.0f,  +1.0f, -1.0f, +1.0f,
     // Left Face
     0.0f, 0.0f,  -1.0f, -1.0f, -1.0f,
     1.0f, 0.0f,  -1.0f, -1.0f, +1.0f,
     1.0f, 1.0f,  -1.0f, +1.0f, +1.0f,
     0.0f, 1.0f,  -1.0f, +1.0f, -1.0f
    };

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

       
    The next block of code is to determine extension-support during run-time.

    First, we can assume that we have a long string containing all supported extensions as 慭n?seperated sub-strings. So all we need to do is to search for a 慭n?and start comparing string with search until we encounter another 慭n?or until string doesn抰 match search anymore. In the first case, return a true for "found", in the other case, take the next sub-string until you encounter the end of string. You抣l have to watch a little bit at the beginning of string, since it does not begin with a newline-character.

    By the way: A common rule is to ALWAYS check during runtime for availability of a given extension!   
       

    bool isInString(char *string, const char *search) {
     int pos=0;
     int maxpos=strlen(search)-1;
     int len=strlen(string);
     char *other;
     for (int i=0; i<len; i++) {
      if ((i==0) || ((i>1) && string[i-1]=='\n')) {   // New Extension Begins Here!
       other=&string[i];
       pos=0;       // Begin New Search
       while (string[i]!='\n') {    // Search Whole Extension-String
        if (string[i]==search[pos]) pos++;  // Next Position
        if ((pos>maxpos) && string[i+1]=='\n') return true; // We Have A Winner!
        i++;
       }
      }
     }
     return false;        // Sorry, Not Found!
    }

       
    Now we have to fetch the extension-string and convert it to be 慭n?separated in order to search it for our desired extension. If we find a sub-string 擥L_ARB_multitexture?in it, this feature is supported. But we only can use it, if __ARB_ENABLE is also true. Last but not least we need GL_EXT_texture_env_combine to be supported. This extension introduces new ways how the texture-units interact. We need this, since GL_ARB_multitexture only feeds the output from one texture unit to the one with the next higher number. So we rather check for this extension than using another complex blending equation (that would not exactly do the same effect!) If all extensions are supported and we are not overridden, we抣l first determine how much texture-units are available, saving them in maxTexelUnits. Then we have to link the functions to our names. This is done by the wglGetProcAdress()-calls with a string naming the function call as parameter and a prototype-cast to ensure we抣l get the correct function type.   
       

    bool initMultitexture(void) {
     char *extensions;
     extensions=strdup((char *) glGetString(GL_EXTENSIONS));   // Fetch Extension String
     int len=strlen(extensions);
     for (int i=0; i<len; i++)      // Separate It By Newline Instead Of Blank
      if (extensions[i]==' ') extensions[i]='\n';

    #ifdef EXT_INFO
     MessageBox(hWnd,extensions,"supported GL extensions",MB_OK | MB_ICONINFORMATION);
    #endif

     if (isInString(extensions,"GL_ARB_multitexture")   // Is Multitexturing Supported?
      && __ARB_ENABLE       // Override Flag
      && isInString(extensions,"GL_EXT_texture_env_combine"))  // texture-environment-combining supported?
     {牋牋牋
      glGetIntegerv(GL_MAX_TEXTURE_UNITS_ARB,&maxTexelUnits);
      glMultiTexCoord1fARB = (PFNGLMULTITEXCOORD1FARBPROC) wglGetProcAddress("glMultiTexCoord1fARB");
      glMultiTexCoord2fARB = (PFNGLMULTITEXCOORD2FARBPROC) wglGetProcAddress("glMultiTexCoord2fARB");
      glMultiTexCoord3fARB = (PFNGLMULTITEXCOORD3FARBPROC) wglGetProcAddress("glMultiTexCoord3fARB");
      glMultiTexCoord4fARB = (PFNGLMULTITEXCOORD4FARBPROC) wglGetProcAddress("glMultiTexCoord4fARB");
      glActiveTextureARB   = (PFNGLACTIVETEXTUREARBPROC) wglGetProcAddress("glActiveTextureARB");
      glClientActiveTextureARB= (PFNGLCLIENTACTIVETEXTUREARBPROC) wglGetProcAddress("glClientActiveTextureARB");
    牋牋牋牋牋牋牋
    #ifdef EXT_INFO
      MessageBox(hWnd,"The GL_ARB_multitexture extension will be used.","feature supported!",MB_OK | MB_ICONINFORMATION);
    #endif

      return true;
     }
     useMultitexture=false;       // We Can't Use It If It Isn't Supported!
     return false;
    }

       
    InitLights() just initialises OpenGL-Lighting and is called by InitGL() later on.   
       

    void initLights(void) {
    牋牋牋?glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient);    // Load Light-Parameters into GL_LIGHT1
    牋牋牋?glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse);
    牋牋牋?glLightfv(GL_LIGHT1, GL_POSITION, LightPosition);
    牋牋牋?glEnable(GL_LIGHT1);
    }

       
    Here we load LOTS of textures. Since auxDIBImageLoad() has an error-handler of it抯 own and since LoadBMP() wasn抰 much predictable without a try-catch-block, I just kicked it. But now to our loading-routine. First, we load the base-bitmap and build three filtered textures out of it ( GL_NEAREST, GL_LINEAR and GL_LINEAR_MIPMAP_NEAREST). Note that I only use one data-structure to hold bitmaps, since we only need one at a time to be open. Over that I introduced a new data-structure called alpha here. It is to hold the alpha-layer of textures, so that I can save RGBA Images as two bitmaps: one 24bpp RGB and one 8bpp greyscale Alpha. For the status-indicator to work properly, we have to delete the Image-block after every load to reset it to NULL.

    Note also, that I use GL_RGB8 instead of just "3" when specifying texture-type. This is to be more conformant to upcoming OpenGL-ICD releases and should always be used instead of just another number. I marked it in orange for you.   
       

    int LoadGLTextures() {        // Load Bitmaps And Convert To Textures
     bool status=true;       // Status Indicator
     AUX_RGBImageRec *Image=NULL;      // Create Storage Space For The Texture
     char *alpha=NULL;

     // Load The Tile-Bitmap for Base-Texture
     if (Image=auxDIBImageLoad("Data/Base.bmp")) {
      glGenTextures(3, texture);     // Create Three Textures

      // Create Nearest Filtered Texture
      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, GL_RGB8, Image->sizeX, Image->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, Image->data);

      // 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, GL_RGB8, Image->sizeX, Image->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, Image->data);

      // 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);
      gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGB8, Image->sizeX, Image->sizeY, GL_RGB, GL_UNSIGNED_BYTE, Image->data);
     }
     else status=false;

     if (Image) {        // If Texture Exists
      if (Image->data) delete Image->data;    // If Texture Image Exists
      delete Image;
      Image=NULL;
     }

       
    Now we抣l load the Bump Map. For reasons discussed later, it has to have only 50% luminance, so we have to scale it in the one or other way. I chose to scale it using the glPixelTransferf()-commands, that specifies how data from bitmaps is converted to textures on pixel-basis. I use it to scale the RGB components of bitmaps to 50%. You should really have a look at the glPixelTransfer()-command family if you抮e not already using them in your programs. They抮e all quite useful.

    Another issue is, that we don抰 want to have our bitmap repeated over and over in the texture. We just want it once, mapping to texture-coordinates (s,t)=(0.0f, 0.0f) thru (s,t)=(1.0f, 1.0f). All other texture-coordinates should be mapped to plain black. This is accomplished by the two glTexParameteri()-calls that are fairly self-explanatory and "clamp" the bitmap in s and t-direction.   
       

     // Load The Bumpmaps
     if (Image=auxDIBImageLoad("Data/Bump.bmp")) {
      glPixelTransferf(GL_RED_SCALE,0.5f);    // Scale RGB By 50%, So That We Have Only
      glPixelTransferf(GL_GREEN_SCALE,0.5f);    // Half Intenstity
      glPixelTransferf(GL_BLUE_SCALE,0.5f);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP); // No Wrapping, Please!
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP);
      glGenTextures(3, bump);      // Create Three Textures

      // Create Nearest Filtered Texture
      >?lt;

      // Create Linear Filtered Texture
      >?lt;

      // Create MipMapped Texture
      >?lt;

       
    You抣l already know this sentence by now: For reasons discussed later, we have to build an inverted Bump Map, luminance at most 50% once again. So we subtract the bumpmap from pure white, which is {255, 255, 255} in integer representation. Since we do NOT set the RGB-Scaling back to 100% (took me about three hours to figure out that this was a major error in my first version!), the inverted bumpmap will be scaled once again to 50% luminance.   
       

      for (int i=0; i<3*Image->sizeX*Image->sizeY; i++)  // Invert The Bumpmap
       Image->data[i]=255-Image->data[i];

      glGenTextures(3, invbump);     // Create Three Textures

      // Create Nearest Filtered Texture
      >?lt;

      // Create Linear Filtered Texture
      >?lt;

      // Create MipMapped Texture
      >?lt;
     }
     else status=false;
     if (Image) {        // If Texture Exists
      if (Image->data) delete Image->data;    // If Texture Image Exists
      delete Image;
      Image=NULL;
     }

       
    Loading the Logo-Bitmaps is pretty much straightforward except for the RGB-A recombining, which should be self-explanatory enough for you to understand. Note that the texture is built from the alpha-memoryblock, not from the Image-memoryblock! Only one filter is used here.   
       

     // Load The Logo-Bitmaps
     if (Image=auxDIBImageLoad("Data/OpenGL_ALPHA.bmp")) {
      alpha=new char[4*Image->sizeX*Image->sizeY];
      // Create Memory For RGBA8-Texture
      for (int a=0; a<Image->sizeX*Image->sizeY; a++)
       alpha[4*a+3]=Image->data[a*3];    // Pick Only Red Value As Alpha!
      if (!(Image=auxDIBImageLoad("Data/OpenGL.bmp"))) status=false;
      for (a=0; a<Image->sizeX*Image->sizeY; a++) {
       alpha[4*a]=Image->data[a*3];    // R
       alpha[4*a+1]=Image->data[a*3+1];   // G
       alpha[4*a+2]=Image->data[a*3+2];   // B
      }

      glGenTextures(1, &glLogo);     // Create One Textures

      // Create Linear Filtered RGBA8-Texture
      glBindTexture(GL_TEXTURE_2D, glLogo);
      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, GL_RGBA8, Image->sizeX, Image->sizeY, 0, GL_RGBA, GL_UNSIGNED_BYTE, alpha);
      delete alpha;
     }
     else status=false;

     if (Image) {        // If Texture Exists
      if (Image->data) delete Image->data;    // If Texture Image Exists
      delete Image;
      Image=NULL;
     }

     // Load The "Extension Enabled"-Logo
     if (Image=auxDIBImageLoad("Data/multi_on_alpha.bmp")) {
      alpha=new char[4*Image->sizeX*Image->sizeY];   // Create Memory For RGBA8-Texture
      >?lt;
      glGenTextures(1, &multiLogo);     // Create One Textures
      // Create Linear Filtered RGBA8-Texture
      >?lt;
      delete alpha;
     }
     else status=false;

     if (Image) {        // If Texture Exists
      if (Image->data) delete Image->data;    // If Texture Image Exists
      delete Image;
      Image=NULL;
     }
     return status;        // Return The Status
    }

       
    Next comes nearly the only unmodified function ReSizeGLScene(). I抳e omitted it here. It is followed by a function doCube() that draws a cube, complete with normalized normals. Note that this version only feeds texture-unit #0, since glTexCoord2f(s,t) is the same thing as glMultiTexCoord2f(GL_TEXTURE0_ARB,s,t). Note also that the cube could be done using interleaved arrays, but this is definitely another issue. Note also that this cube CAN NOT be done using a display list, since display-lists seem to use an internal floating point accuracy different from GLfloat. Since this leads to several nasty effects, generally referred to as "decaling"-problems, I kicked display lists. I assume that a general rule for multipass algorithms is to do the entire geometry with or without display lists. So never dare mixing even if it seems to run on your hardware, since it won抰 run on any hardware!   
       

    GLvoid ReSizeGLScene(GLsizei width, GLsizei height)
    // Resize And Initialize The GL Window
    >?lt;

    void doCube (void) {
     int i;
     glBegin(GL_QUADS);
      // Front Face
      glNormal3f( 0.0f, 0.0f, +1.0f);
      for (i=0; i<4; i++) {
       glTexCoord2f(data[5*i],data[5*i+1]);
       glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
      }
      // Back Face
      glNormal3f( 0.0f, 0.0f,-1.0f);
      for (i=4; i<8; i++) {
       glTexCoord2f(data[5*i],data[5*i+1]);
       glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
      }
      // Top Face
      glNormal3f( 0.0f, 1.0f, 0.0f);
      for (i=8; i<12; i++) {
       glTexCoord2f(data[5*i],data[5*i+1]);
       glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
      }
      // Bottom Face
      glNormal3f( 0.0f,-1.0f, 0.0f);
      for (i=12; i<16; i++) {
       glTexCoord2f(data[5*i],data[5*i+1]);
       glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
      }
      // Right Face
      glNormal3f( 1.0f, 0.0f, 0.0f);
      for (i=16; i<20; i++) {
       glTexCoord2f(data[5*i],data[5*i+1]);
       glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
      }
      // Left Face
      glNormal3f(-1.0f, 0.0f, 0.0f);
      for (i=20; i<24; i++) {
       glTexCoord2f(data[5*i],data[5*i+1]);
       glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
      }
     glEnd();
    }

       
    Time to initialize OpenGL. All as in Lesson 06, except that I call initLights() instead of setting them here. Oh, and of course I抦 calling Multitexture-setup, here!   
       

    int InitGL(GLvoid)        // All Setup For OpenGL Goes Here
    {
     multitextureSupported=initMultitexture();
     if (!LoadGLTextures()) return false;     // Jump To Texture Loading Routine
     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

     initLights();        // Initialize OpenGL Light
     return true        // Initialization Went OK
    }

       
    Here comes about 95% of the work. All references like "for reasons discussed later" will be solved in the following block of theory.

    Begin Theory ( Emboss Bump Mapping )

    If you have a Powerpoint-viewer installed, it is highly recommended that you download the following presentation:

    "Emboss Bump Mapping" by Michael I. Gold, nVidia Corp. [.ppt, 309K]

    For those without Powerpoint-viewer, I抳e tried to convert the information contained in the document to .html-format. Here it comes:

    Emboss Bump Mapping

    Michael I. Gold

    NVidia Corporation

    Bump Mapping

    Real Bump Mapping Uses Per-Pixel Lighting.
    Lighting calculation at each pixel based on perturbed normal vectors.
    Computationally expensive.
    For more information see: Blinn, J. : Simulation of Wrinkled Surfaces, Computer Graphics. 12,3 (August 1978) 286-292.
    For information on the web go to: http://www.objectecture.com/ to see Cass Everitt抯 Orthogonal Illumination Thesis. (rem.: Jens)
    Emboss Bump Mapping

    Emboss Bump Mapping Is A Hack
    Diffuse lighting only, no specular component
    Under-sampling artefacts (may result in blurry motion, rem.: Jens)
    Possible on today抯 consumer hardware (as shown, rem.: Jens)
    If it looks good, do it!
    Diffuse Lighting Calculation

    C=(L*N) x Dl x Dm
    L is light vector
    N is normal vector
    Dl is light diffuse color
    Dm is material diffuse color
    Bump Mapping changes N per pixel
    Emboss Bump Mapping approximates (L*N)
    Approximate Diffuse Factor L*N

    Texture Map Represents Heightfield
    [0,1] represents range of bump function
    First derivate represents slope m (Note that m is only 1D. Imagine m to be the inf.-norm of grad(s,t) to a given set of coordinates (s,t)!, rem.: Jens)
    m increases / decreases base diffuse factor Fd
    (Fd+m) approximates (L*N) per pixel
    Approximate Derivative

    Embossing Approximates Derivative
    Lookup height H0 at point (s,t)
    Lookup height H1 at point slightly perturbed toward light source (s+ds,t+dt)
    Subtract original height H0 from perturbed height H1
    Difference represents instantaneous slope m=H1-H0
    Compute The Bump

    1) Original bump (H0).

    2) Original bump (H0) overlaid with second bump (H1) slightly perturbed toward light source.

    3) Substract original bump from second (H0-H1). This leads to brightened (B) and darkened (D) areas.

    Compute The Lighting

    Evaluate Fragment Color Cf
    Cf = (L*N) x Dl x Dm
    (L*N) ~ (Fd + (H1-H0))
    Dm x Dl is encoded in surface texture Ct. Could control Dl seperately, if you抮e clever. (we control it using OpenGL-Lighting!, rem.: Jens)
    Cf = (Fd + (H0-H1) x Ct
    Is That All? It抯 So Easy!

    We抮e Not Quite Done Yet. We Still Must:
    Build a texture (using a painting program, rem.: Jens)
    Calculate texture coordinate offsets (ds,dt)
    Calculate diffuse Factor Fd (is controlled using OpenGL-Lighting!, rem.: Jens)
    Both are derived from normal N and light vector L (in our case, only (ds,dt) are calculated explicitly!, rem.: Jens)
    Now we have to do some math
    Building A Texture

    Conserve Textures!
    Current multitexture-hardware only supports two textures! (By now, not true anymore, but nevertheless you should read this!, rem.: Jens)
    Bump Map in ALPHA channel (not the way we do it, could implement it yourself as an exercise if you have TNT-chipset rem.: Jens)
    Maximum bump = 1.0
    Level ground = 0.5
    Maximum depression = 0.0
    Surface color in RGB channels
    Set internal format to GL_RGBA8 !!
    Calculate Texture Offsets

    Rotate Light Vector Into Normal Space
    Need Normal coordinate system
    Derive coordinate system from normal and 搖p?vector (we pass the texCoord directions to our offset generator explicitly, rem.: Jens)
    Normal is z-axis
    Cross-product is x-axis
    Throw away "up" vector, derive y-axis as cross-product of x- and z-axis
    Build 3x3 matrix Mn from axes
    Transform light vector into normal space.(Mn is also called an orthonormal basis. Think of Mn*v as to "express" v in means of a basis describing tangent space rather than in means of the standard basis. Note also that orthonormal bases are invariant against-scaling resulting in no loss of normalization when multiplying vectors! rem.: Jens)
    Calculate Texture Offsets (Cont抎)

    Use Normal-Space Light Vector For Offsets
    L?= Mn x L
    Use L抶, L抷 for (ds,dt)
    Use L抸 for diffuse factor! (Rather not! If you抮e no TNT-owner, use OpenGL-Lighting instead, since you have to do one additional pass anyhow!, rem.: Jens)
    If light vector is near normal, L抶, L抷 are small.
    If light vector is near tangent plane, L抶, L抷 are large.
    What if L抸 is less than zero?
    Light is on opposite side from normal
    Fade contribution toward zero.
    Implementation On TNT

    Calculate Vectors, Texcoords On The Host
    Pass diffuse factor as vertex alpha
    Could use vertex color for light diffuse color
    H0 and surface color from texture unit 0
    H1 from texture unit 1 (same texture, different coordinates)
    ARB_multitexture extension
    Combines extension (more precisely: the NVIDIA_multitexture_combiners extension, featured by all TNT-family cards, rem.: Jens)
    Implementation on TNT (Cont'd)

    Combiner 0 Alpha-Setup:
    (1-T0a) + T1a - 0.5 (T0a stands for "texture-unit 0, alpha channel", rem.: Jens)
    (T1a-T0a) maps to (-1,1), but hardware clamps to (0,1)
    0.5 bias balances the loss from clamping (consider using 0.5 scale, since you can use a wider variety of bump maps, rem.: Jens)
    Could modulate light diffuse color with T0c
    Combiner 0 rgb-setup:
    (T0c * C0a + T0c * Fda - 0.5 )*2
    0.5 bias balances the loss from clamping
    scale by 2 brightens the image
    End Theory ( Emboss Bump Mapping )

    Though we抮e doing it a little bit different than the TNT-Implementation to enable our program to run on ALL accelerators, we can learn two or three things here. One thing is, that bump mapping is a multi-pass algorithm on most cards (not on TNT-family, where it can be implemented in one 2-texture pass.) You should now be able to imagine how nice multitexturing really is. We抣l now implement a 3-pass non-multitexture algorithm, that can be (and will be) developed into a 2-pass multitexture algorithm.

    By now you should be aware, that we抣l have to do some matrix-matrix-multiplication (and matrix-vector-multiplication, too). But that抯 nothing to worry about: OpenGL will do the matrix-matrix-multiplication for us (if tweaked right) and the matrix-vector-multiplication is really easy-going: VMatMult(M,v) multiplies matrix M with vector v and stores the result back in v: v:=M*v. All Matrices and vectors passed have to be in homogenous-coordinates resulting in 4x4 matrices and 4-dim vectors. This is to ensure conformity to OpenGL in order to multiply own vectors with OpenGL-matrices right away.   
       

    // Calculates v=vM, M Is 4x4 In Column-Major, v Is 4dim. Row (i.e. "Transposed")
    void VMatMult(GLfloat *M, GLfloat *v) {
     GLfloat res[3];
     res[0]=M[ 0]*v[0]+M[ 1]*v[1]+M[ 2]*v[2]+M[ 3]*v[3];
     res[1]=M[ 4]*v[0]+M[ 5]*v[1]+M[ 6]*v[2]+M[ 7]*v[3];
     res[2]=M[ 8]*v[0]+M[ 9]*v[1]+M[10]*v[2]+M[11]*v[3];
     v[0]=res[0];
     v[1]=res[1];
     v[2]=res[2];
     v[3]=M[15];        // Homogenous Coordinate
    }

       
    Begin Theory ( Emboss Bump Mapping Algorithms )

    Here we抣l discuss two different algorithms. I found the first one several days ago under:
    http://www.nvidia.com/marketing/Developer/DevRel.nsf/TechnicalDemosFrame?OpenPage

    The program is called GL_BUMP and was written by Diego T醨tara in 1999.
    It implements really nice looking bump mapping, though it has some drawbacks.
    But first, lets have a look at T醨tara抯 Algorithm:
    All vectors have to be EITHER in object OR world space
    Calculate vector v from current vertex to light position
    Normalize v
    Project v into tangent space. (This is the plane touching the surface in the current vertex. Typically, if working with flat surfaces, this is the surface itself).
    Offset (s,t)-coordinates by the projected v抯 x and y component
    This looks not bad! It is basically the Algorithm introduced by Michael I. Gold above. But it has a major drawback: T醨tara only does the projection for a xy-plane! This is not sufficient for our purposes since it simplifies the projection step to just taking the xy-components of v and discarding the z-component.

    But his implementation does the diffuse lighting the same way we抣l do it: by using OpenGL抯 built-in lighting. Since we can抰 use the combiners-method Gold suggests (we want our programs to run anywhere, not just on TNT-cards!), we can抰 store the diffuse factor in the alpha channel. Since we already have a 3-pass non-multitexture / 2-pass multitexture problem, why not apply OpenGL-Lighting to the last pass to do all the ambient light and color stuff for us? This is possible (and looks quite well) only because we have no complex geometry, so keep this in mind. If you抎 render several thousands of bump mapped triangles, try to invent something new!

    Furthermore, he uses multitexturing, which is, as we shall see, not as easy as you might have thought regarding this special case.

    But now to our Implementation. It looks quite the same to the above Algorithm, except for the projection step, where we use an own approach:


    We use OBJECT COORDINATES, this means we don抰 apply the modelview matrix to our calculations. This has a nasty side-effect: since we want to rotate the cube, object-coordinates of the cube don抰 change, world-coordinates (also referred to as eye-coordinates) do. But our light-position should not be rotated with the cube, it should be just static, meaning that it抯 world-coordinates don抰 change. To compensate, we抣l apply a trick commonly used in computer graphics: Instead of transforming each vertex to worldspace in advance to computing the bumps, we抣l just transform the light into object-space by applying the inverse of the modelview-matrix. This is very cheap in this case since we know exactly how the modelview-matrix was built step-by-step, so an inversion can also be done step-by-step. We抣l come back later to that issue.
    We calculate the current vertex c on our surface (simply by looking it up in data).
    Then we抣l calculate a normal n with length 1 (We usually know n for each face of a cube!). This is important, since we can save computing time by requesting normalized vectors. Calculate the light vector v from c to the light position l
    If there抯 work to do, build a matrix Mn representing the orthonormal projection. This is done as f
    Calculate out texture coordinate offset by multiplying the supplied texture-coordinate directions s and t each with v and MAX_EMBOSS: ds = s*v*MAX_EMBOSS, dt=t*v*MAX_EMBOSS. Note that s, t and v are vectors while MAX_EMBOSS isn抰.
    Add the offset to the texture-coordinates in pass 2.
    Why this is good:
    Fast (only needs one squareroot and a couple of MULs per vertex)!
    Looks very nice!
    This works with all surfaces, not just planes.
    This runs on all accelerators.
    Is glBegin/glEnd friendly: Does not need any "forbidden" GL-commands.
    Drawback:
    Not fully physical correct.
    Leaves minor artefacts.

    This figure shows where our vectors are located. You can get t and s by simply subtracting adjacent vertices, but be sure to have them point in the right direction and to normalize them. The blue spot marks the vertex where texCoord2f(0.0f,0.0f) is mapped to.

    End Theory ( Emboss Bump Mapping Algorithms )

    Let抯 have a look to texture-coordinate offset generation, first. The function is called SetUpBumps(), since this actually is what it does:   
       

    // Sets Up The Texture-Offsets
    // n : Normal On Surface. Must Be Of Length 1
    // c : Current Vertex On Surface
    // l : Lightposition
    // s : Direction Of s-Texture-Coordinate In Object Space (Must Be Normalized!)
    // t : Direction Of t-Texture-Coordinate In Object Space (Must Be Normalized!)
    void SetUpBumps(GLfloat *n, GLfloat *c, GLfloat *l, GLfloat *s, GLfloat *t) {
     GLfloat v[3];        // Vertex From Current Position To Light
     GLfloat lenQ;        // Used To Normalize
     // Calculate v From Current Vertex c To Lightposition And Normalize v
     v[0]=l[0]-c[0];
     v[1]=l[1]-c[1];
     v[2]=l[2]-c[2];
     lenQ=(GLfloat) sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);
     v[0]/=lenQ;
     v[1]/=lenQ;
     v[2]/=lenQ;
     // Project v Such That We Get Two Values Along Each Texture-Coordinate Axis
     c[0]=(s[0]*v[0]+s[1]*v[1]+s[2]*v[2])*MAX_EMBOSS;
     c[1]=(t[0]*v[0]+t[1]*v[1]+t[2]*v[2])*MAX_EMBOSS;

       
    Doesn抰 look that complicated anymore, eh? But theory is necessary to understand and control this effect. (I learned THAT myself during writing this tutorial).

    I always like logos to be displayed while presentational programs are running. We抣l have two of them right now. Since a call to doLogo() resets the GL_MODELVIEW-matrix, this has to be called as final rendering pass.

    This function displays two logos: An OpenGL-Logo and a multitexture-Logo, if this feature is enabled. The logos are alpha-blended and are sort of semi-transparent. Since they have an alpha-channel, I blend them using GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, as suggested by all OpenGL-documentation. Since they are all co-planar, we do not have to z-sort them before. The numbers that are used for the vertices are "empirical" (a.k.a. try-and-error) to place them neatly into the screen edges. We抣l have to enable blending and disable lighting to avoid nasty effects. To ensure they抮e in front of all, just reset the GL_MODELVIEW-matrix and set depth-function to GL_ALWAYS.   
       

    void doLogo(void) {
     // MUST CALL THIS LAST!!!, Billboards The Two Logos
     glDepthFunc(GL_ALWAYS);
     glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
     glEnable(GL_BLEND);
     glDisable(GL_LIGHTING);
     glLoadIdentity();
     glBindTexture(GL_TEXTURE_2D,glLogo);
     glBegin(GL_QUADS);
      glTexCoord2f(0.0f,0.0f); glVertex3f(0.23f, -0.4f,-1.0f);
      glTexCoord2f(1.0f,0.0f); glVertex3f(0.53f, -0.4f,-1.0f);
      glTexCoord2f(1.0f,1.0f); glVertex3f(0.53f, -0.25f,-1.0f);
      glTexCoord2f(0.0f,1.0f); glVertex3f(0.23f, -0.25f,-1.0f);
     glEnd();
     if (useMultitexture) {
      glBindTexture(GL_TEXTURE_2D,multiLogo);
      glBegin(GL_QUADS);
       glTexCoord2f(0.0f,0.0f); glVertex3f(-0.53f, -0.25f,-1.0f);
       glTexCoord2f(1.0f,0.0f); glVertex3f(-0.33f, -0.25f,-1.0f);
       glTexCoord2f(1.0f,1.0f); glVertex3f(-0.33f, -0.15f,-1.0f);
       glTexCoord2f(0.0f,1.0f); glVertex3f(-0.53f, -0.15f,-1.0f);
      glEnd();
     }
    }

       
    Here comes the function for doing the bump mapping without multitexturing. It抯 a three-pass implementation. As a first step, the GL_MODELVIEW matrix is inverted by applying to the identity-matrix all steps later applied to the GL_MODELVIEW in reverse order and inverted. The result is a matrix that "undoes" the GL_MODELVIEW if applied to an object. We fetch it from OpenGL by simply using glGetFloatv(). Remember that the matrix has to be an array of 16 and that the matrix is "transposed"!

    By the way: If you don抰 exactly know how the modelview was built, consider using world-space, since matrix-inversion is complicated and costly. But if you抮e doing large amounts of vertices inverting the modelview with a more generalized approach could be faster.   
       

    bool doMesh1TexelUnits(void) {
     GLfloat c[4]={0.0f,0.0f,0.0f,1.0f};     // Holds Current Vertex
     GLfloat n[4]={0.0f,0.0f,0.0f,1.0f};     // Normalized Normal Of Current Surface
     GLfloat s[4]={0.0f,0.0f,0.0f,1.0f};     // s-Texture Coordinate Direction, Normalized
     GLfloat t[4]={0.0f,0.0f,0.0f,1.0f};     // t-Texture Coordinate Direction, Normalized
     GLfloat l[4];        // Holds Our Lightposition To Be Transformed Into Object Space
     GLfloat Minv[16];       // Holds The Inverted Modelview Matrix To Do So
     int i;

     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);   // Clear The Screen And The Depth Buffer

     // Build Inverse Modelview Matrix First. This Substitutes One Push/Pop With One glLoadIdentity();
     // Simply Build It By Doing All Transformations Negated And In Reverse Order
     glLoadIdentity();
     glRotatef(-yrot,0.0f,1.0f,0.0f);
     glRotatef(-xrot,1.0f,0.0f,0.0f);
     glTranslatef(0.0f,0.0f,-z);
     glGetFloatv(GL_MODELVIEW_MATRIX,Minv);
     glLoadIdentity();
     glTranslatef(0.0f,0.0f,z);
     glRotatef(xrot,1.0f,0.0f,0.0f);
     glRotatef(yrot,0.0f,1.0f,0.0f);

     // Transform The Lightposition Into Object Coordinates:
     l[0]=LightPosition[0];
     l[1]=LightPosition[1];
     l[2]=LightPosition[2];
     l[3]=1.0f;        // Homogenous Coordinate
     VMatMult(Minv,l);

       
    First Pass:
    Use bump-texture
    Disable Blending
    Disable Lighting
    Use non-offset texture-coordinates
    Do the geometry
    This will render a cube only consisting out of bump map.   
       

     glBindTexture(GL_TEXTURE_2D, bump[filter]);
     glDisable(GL_BLEND);
     glDisable(GL_LIGHTING);
     doCube();

       
    Second Pass:
    Use inverted bump-texture
    Enable Blending GL_ONE, GL_ONE
    Keep Lighting disabled
    Use offset texture-coordinates (This means that you call SetUpBumps() before each face of the cube
    Do the geometry
    This will render a cube with the correct emboss bump mapping, but without colors.

    You could save computing time by just rotating the lightvector into inverted direction. However, this didn抰 work out correctly, so we do it the plain way: rotate each normal and center-point the same way we rotate our geometry!   
       

     glBindTexture(GL_TEXTURE_2D,invbump[filter]);
     glBlendFunc(GL_ONE,GL_ONE);
     glDepthFunc(GL_LEQUAL);
     glEnable(GL_BLEND);

     glBegin(GL_QUADS);
      // Front Face
      n[0]=0.0f;
      n[1]=0.0f;
      n[2]=1.0f;
      s[0]=1.0f;
      s[1]=0.0f;
      s[2]=0.0f;
      t[0]=0.0f;
      t[1]=1.0f;
      t[2]=0.0f;
      for (i=0; i<4; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // Back Face
      n[0]=0.0f;
      n[1]=0.0f;
      n[2]=-1.0f;
      s[0]=-1.0f;
      s[1]=0.0f;
      s[2]=0.0f;
      t[0]=0.0f;
      t[1]=1.0f;
      t[2]=0.0f;
      for (i=4; i<8; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // Top Face
      n[0]=0.0f;
      n[1]=1.0f;
      n[2]=0.0f;
      s[0]=1.0f;
      s[1]=0.0f;
      s[2]=0.0f;
      t[0]=0.0f;
      t[1]=0.0f;
      t[2]=-1.0f;
      for (i=8; i<12; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // Bottom Face
      n[0]=0.0f;
      n[1]=-1.0f;
      n[2]=0.0f;
      s[0]=-1.0f;
      s[1]=0.0f;
      s[2]=0.0f;
      t[0]=0.0f;
      t[1]=0.0f;
      t[2]=-1.0f;
      for (i=12; i<16; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // Right Face
      n[0]=1.0f;
      n[1]=0.0f;
      n[2]=0.0f;
      s[0]=0.0f;
      s[1]=0.0f;
      s[2]=-1.0f;
      t[0]=0.0f;
      t[1]=1.0f;
      t[2]=0.0f;
      for (i=16; i<20; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // Left Face
      n[0]=-1.0f;
      n[1]=0.0f;
      n[2]=0.0f;
      s[0]=0.0f;
      s[1]=0.0f;
      s[2]=1.0f;
      t[0]=0.0f;
      t[1]=1.0f;
      t[2]=0.0f;
      for (i=20; i<24; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
     glEnd();

       
    Third Pass:
    Use (colored) base-texture
    Enable Blending GL_DST_COLOR, GL_SRC_COLOR
    This blending equation multiplies by 2: (Cdst*Csrc)+(Csrc*Cdst)=2(Csrc*Cdst)!
    Enable Lighting to do the ambient and diffuse stuff
    Reset GL_TEXTURE-matrix to go back to "normal" texture coordinates
    Do the geometry
    This will finish cube-rendering, complete with lighting. Since we can switch back and forth between multitexturing and non-multitexturing, we have to reset the texture-environment to "normal" GL_MODULATE first. We only do the third pass, if the user doesn抰 want to see just the emboss.   
       

     if (!emboss) {
      glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
      glBindTexture(GL_TEXTURE_2D,texture[filter]);
      glBlendFunc(GL_DST_COLOR,GL_SRC_COLOR);
      glEnable(GL_LIGHTING);
      doCube();
     }

       
    Last Pass:
    update geometry (esp. rotations)
    do the Logos
      
       

     xrot+=xspeed;
     yrot+=yspeed;
     if (xrot>360.0f) xrot-=360.0f;
     if (xrot<0.0f) xrot+=360.0f;
     if (yrot>360.0f) yrot-=360.0f;
     if (yrot<0.0f) yrot+=360.0f;

     /* LAST PASS: Do The Logos! */
     doLogo();
     return true;        // Keep Going
    }

       
    This function will do the whole mess in 2 passes with multitexturing support. We support two texel-units. More would be extreme complicated due to the blending equations. Better trim to TNT instead. Note that almost the only difference to doMesh1TexelUnits() is, that we send two sets of texture-coordinates for each vertex!   
       

    bool doMesh2TexelUnits(void) {
     GLfloat c[4]={0.0f,0.0f,0.0f,1.0f};     // Holds Current Vertex
     GLfloat n[4]={0.0f,0.0f,0.0f,1.0f};     // Normalized Normal Of Current Surface
     GLfloat s[4]={0.0f,0.0f,0.0f,1.0f};     // s-Texture Coordinate Direction, Normalized
     GLfloat t[4]={0.0f,0.0f,0.0f,1.0f};     // t-Texture Coordinate Direction, Normalized
     GLfloat l[4];        // Holds Our Lightposition To Be Transformed Into Object Space
     GLfloat Minv[16];       // Holds The Inverted Modelview Matrix To Do So
     int i;

     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);   // Clear The Screen And The Depth Buffer

     // Build Inverse Modelview Matrix First. This Substitutes One Push/Pop With One glLoadIdentity();
     // Simply Build It By Doing All Transformations Negated And In Reverse Order
     glLoadIdentity();
     glRotatef(-yrot,0.0f,1.0f,0.0f);
     glRotatef(-xrot,1.0f,0.0f,0.0f);
     glTranslatef(0.0f,0.0f,-z);
     glGetFloatv(GL_MODELVIEW_MATRIX,Minv);
     glLoadIdentity();
     glTranslatef(0.0f,0.0f,z);

     glRotatef(xrot,1.0f,0.0f,0.0f);
     glRotatef(yrot,0.0f,1.0f,0.0f);

     // Transform The Lightposition Into Object Coordinates:
     l[0]=LightPosition[0];
     l[1]=LightPosition[1];
     l[2]=LightPosition[2];
     l[3]=1.0f;        // Homogenous Coordinate
     VMatMult(Minv,l);

       
    First Pass:
    No Blending
    No Lighting
    Set up the texture-combiner 0 to
    Use bump-texture
    Use not-offset texture-coordinates
    Texture-Operation GL_REPLACE, resulting in texture just being drawn
    Set up the texture-combiner 1 to
    Offset texture-coordinates
    Texture-Operation GL_ADD, which is the multitexture-equivalent to ONE, ONE- blending.
    This will render a cube consisting out of the grey-scale erode map.   
       

     // TEXTURE-UNIT #0
     glActiveTextureARB(GL_TEXTURE0_ARB);
     glEnable(GL_TEXTURE_2D);
     glBindTexture(GL_TEXTURE_2D, bump[filter]);
     glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_EXT);
     glTexEnvf (GL_TEXTURE_ENV, GL_COMBINE_RGB_EXT, GL_REPLACE);

     // TEXTURE-UNIT #1
     glActiveTextureARB(GL_TEXTURE1_ARB);
     glEnable(GL_TEXTURE_2D);
     glBindTexture(GL_TEXTURE_2D, invbump[filter]);
     glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_EXT);
     glTexEnvf (GL_TEXTURE_ENV, GL_COMBINE_RGB_EXT, GL_ADD);

     // General Switches
     glDisable(GL_BLEND);
     glDisable(GL_LIGHTING);

       
    Now just render the faces one by one, as already seen in doMesh1TexelUnits(). Only new thing: Uses glMultiTexCoor2fARB() instead of just glTexCoord2f(). Note that you must specify which texture-unit you mean by the first parameter, which must be GL_TEXTUREi_ARB with i in [0..31]. (What hardware has 32 texture-units? And what for?)   
       

     glBegin(GL_QUADS);
      // Front Face
      n[0]=0.0f;
      n[1]=0.0f;
      n[2]=1.0f;
      s[0]=1.0f;
      s[1]=0.0f;
      s[2]=0.0f;
      t[0]=0.0f;
      t[1]=1.0f;
      t[2]=0.0f;
      for (i=0; i<4; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
       glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // Back Face
      n[0]=0.0f;
      n[1]=0.0f;
      n[2]=-1.0f;
      s[0]=-1.0f;
      s[1]=0.0f;
      s[2]=0.0f;
      t[0]=0.0f;
      t[1]=1.0f;
      t[2]=0.0f;
      for (i=4; i<8; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
       glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // Top Face
      n[0]=0.0f;
      n[1]=1.0f;
      n[2]=0.0f;
      s[0]=1.0f;
      s[1]=0.0f;
      s[2]=0.0f;
      t[0]=0.0f;
      t[1]=0.0f;
      t[2]=-1.0f;
      for (i=8; i<12; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
       glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // Bottom Face
      n[0]=0.0f;
      n[1]=-1.0f;
      n[2]=0.0f;
      s[0]=-1.0f;
      s[1]=0.0f;
      s[2]=0.0f;
      t[0]=0.0f;
      t[1]=0.0f;
      t[2]=-1.0f;
      for (i=12; i<16; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
       glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // Right Face
      n[0]=1.0f;
      n[1]=0.0f;
      n[2]=0.0f;
      s[0]=0.0f;
      s[1]=0.0f;
      s[2]=-1.0f;
      t[0]=0.0f;
      t[1]=1.0f;
      t[2]=0.0f;
      for (i=16; i<20; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
       glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
      // Left Face
      n[0]=-1.0f;
      n[1]=0.0f;
      n[2]=0.0f;
      s[0]=0.0f;
      s[1]=0.0f;
      s[2]=1.0f;
      t[0]=0.0f;
      t[1]=1.0f;
      t[2]=0.0f;
      for (i=20; i<24; i++) {
       c[0]=data[5*i+2];
       c[1]=data[5*i+3];
       c[2]=data[5*i+4];
       SetUpBumps(n,c,l,s,t);
       glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
       glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
       glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
      }
     glEnd();

       
    Second Pass
    Use the base-texture
    Enable Lighting
    No offset texturre-coordinates => reset GL_TEXTURE-matrix
    Reset texture environment to GL_MODULATE in order to do OpenGLLighting (doesn抰 work otherwise!)
    This will render our complete bump-mapped cube.   
       

     glActiveTextureARB(GL_TEXTURE1_ARB);
     glDisable(GL_TEXTURE_2D);
     glActiveTextureARB(GL_TEXTURE0_ARB);
     if (!emboss) {
      glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
      glBindTexture(GL_TEXTURE_2D,texture[filter]);
      glBlendFunc(GL_DST_COLOR,GL_SRC_COLOR);
      glEnable(GL_BLEND);
      glEnable(GL_LIGHTING);
      doCube();
     }

       
    Last Pass
    Update Geometry (esp. rotations)
    Do The Logos
      
       

     xrot+=xspeed;
     yrot+=yspeed;
     if (xrot>360.0f) xrot-=360.0f;
     if (xrot<0.0f) xrot+=360.0f;
     if (yrot>360.0f) yrot-=360.0f;
     if (yrot<0.0f) yrot+=360.0f;

     /* LAST PASS: Do The Logos! */
     doLogo();
     return true;        // Keep Going
    }

       
    Finally, a function to render the cube without bump mapping, so that you can see what difference this makes!   
       

    bool doMeshNoBumps(void) {
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);   // Clear The Screen And The Depth Buffer
     glLoadIdentity();       // Reset The View
     glTranslatef(0.0f,0.0f,z);

     glRotatef(xrot,1.0f,0.0f,0.0f);
     glRotatef(yrot,0.0f,1.0f,0.0f);

     if (useMultitexture) {
      glActiveTextureARB(GL_TEXTURE1_ARB);
      glDisable(GL_TEXTURE_2D);
      glActiveTextureARB(GL_TEXTURE0_ARB);
     }

     glDisable(GL_BLEND);
     glBindTexture(GL_TEXTURE_2D,texture[filter]);
     glBlendFunc(GL_DST_COLOR,GL_SRC_COLOR);
     glEnable(GL_LIGHTING);
     doCube();

     xrot+=xspeed;
     yrot+=yspeed;
     if (xrot>360.0f) xrot-=360.0f;
     if (xrot<0.0f) xrot+=360.0f;
     if (yrot>360.0f) yrot-=360.0f;
     if (yrot<0.0f) yrot+=360.0f;

     /* LAST PASS: Do The Logos! */
     doLogo();
     return true;        // Keep Going
    }

       
    All the drawGLScene() function has to do is to determine which doMesh-function to call:   
       

    bool DrawGLScene(GLvoid)       // Here's Where We Do All The Drawing
    {
     if (bumps) {
      if (useMultitexture && maxTexelUnits>1)
       return doMesh2TexelUnits();
      else return doMesh1TexelUnits();  }
     else return doMeshNoBumps();
    }

       
    Kills the GLWindow, not modified (thus omitted):   
       

    GLvoid KillGLWindow(GLvoid)       // Properly Kill The Window
    >?lt;

       
    Creates the GLWindow, not modified (thus omitted):   
       

    BOOL CreateGLWindow(char* title, int width, int height, int bits, bool fullscreenflag)
    >?lt;

       
    Windows main-loop, not modified (thus omitted):   
       

    LRESULT CALLBACK WndProc( HWND hWnd,     // Handle For This Window
        UINT uMsg,     // Message For This Window
        WPARAM wParam,     // Additional Message Information
        LPARAM lParam)     // Additional Message Information
    >?lt;

       
    Windows main-function, added some keys:
    E: Toggle Emboss / Bumpmapped Mode
    M: Toggle Multitexturing
    B: Toggle Bumpmapping. This Is Mutually Exclusive With Emboss Mode
    F: Toggle Filters. You抣l See Directly That GL_NEAREST Isn抰 For Bumpmapping
    CURSOR-KEYS: Rotate The Cube
      
       

    int WINAPI WinMain( HINSTANCE hInstance,     // Instance
       HINSTANCE hPrevInstance,    // Previous Instance
       LPSTR lpCmdLine,     // Command Line Parameters
       int nCmdShow)      // Window Show State
    {

     >?lt;

        if (keys['E'])
        {
         keys['E']=false;
         emboss=!emboss;
        }

        if (keys['M'])
        {
         keys['M']=false;
         useMultitexture=((!useMultitexture) && multitextureSupported);
        }

        if (keys['B'])
        {
         keys['B']=false;
         bumps=!bumps;
        }

        if (keys['F'])
        {
         keys['F']=false;
         filter++;
         filter%=3;
        }

        if (keys[VK_PRIOR])
        {
         z-=0.02f;
        }

        if (keys[VK_NEXT])
        {
         z+=0.02f;
        }

        if (keys[VK_UP])
        {
         xspeed-=0.01f;
        }

        if (keys[VK_DOWN])
        {
         xspeed+=0.01f;
        }

        if (keys[VK_RIGHT])
        {
         yspeed+=0.01f;
        }

        if (keys[VK_LEFT])
        {
         yspeed-=0.01f;
        }
       }
      }
     }
     // Shutdown
     KillGLWindow();        // Kill The Window
     return (msg.wParam);       // Exit The Program
    }

       
    Now that you managed this tutorial some words about generating textures and bumpmapped objects before you start to program mighty games and wonder why bumpomapping isn抰 that fast or doesn抰 look that good:
    You shouldn抰 use textures of 256x256 as done in this lesson. This slows things down a lot. Only do so if demonstrating visual capabilities (like in tutorials).
    A bumpmapped cube is not usual. A rotated cube far less. The reason for this is the viewing angle: The steeper it gets, the more visual distortion due to filtering you get. Nearly all multipass algorithms are very affected by this. To avoid the need for high-resolution textures, reduce the minimum viewing angle to a sensible value or reduce the bandwidth of viewing angles and pre-filter you texture to perfectly fit that bandwidth.
    You should first have the colored-texture. The bumpmap can be often derived from it using an average paint-program and converting it to grey-scale.
    The bumpmap should be "sharper" and higher in contrast than the color-texture. This is usually done by applying a "sharpening filter" to the texture and might look strange at first, but believe me: you can sharpen it A LOT in order to get first class visual appearance.
    The bumpmap should be centered around 50%-grey (RGB=127,127,127), since this means "no bump at all", brighter values represent ing bumps and lower "scratches". This can be achieved using "histogram" functions in some paint-programs.
    The bumpmap can be one fourth in size of the color-texture without "killing" visual appearance, though you抣l definitely see the difference.
    Now you should at least have a basic understanding of the issued covered in this tutorial. I hope you have enjoyed reading it.

    If you have questions and / or suggestions regarding this lesson, you can just mail me, since I have not yet a web page.

    This is my current project and will follow soon.

    Thanks must go to:
    Michael I. Gold for his Bump Mapping Documentation
    Diego T醨tara for his example code
    NVidia for putting great examples on the WWW
    And last but not least to NeHe who helped me learn a lot about OpenGL.
    Jens Schneider

    Jeff Molofee (NeHe)

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

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

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

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

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