Profilo di YiStone-此人已死FotoBlogElenchiAltro Strumenti Guida

Jeff Molofee的OPENGL教程-简单的透明

OpenGL中的绝大多数特效都与某些类型的(色彩)混合有关。混色的定义为,将某个象素的颜色和已绘制在屏幕上与其对应的象素颜色相互结合。至于如何结合这两个颜色则依赖于颜色的alpha通道的分量值,以及/或者所使用的混色函数。Alpha通常是位于颜色值末尾的第4个颜色组成分量。前面这些课我们都是用GL_RGB来指定颜色的三个分量。相应的GL_RGBA可以指定alpha分量的值。更进一步,我们可以使用glColor4f()来代替glColor3f()
绝大多数人都认为Alpha分量代表材料的透明度。这就是说,alpha值为0.0时所代表的材料是完全透明的。alpha值为1.0时所代表的材料则是完全不透明的。

混色的公式

若您对数学不感冒,而只想看看如何实现透明,请跳过这一节。若您想深入理解(色彩)混合的工作原理,这一节应该适合您吧。译者注:其实并不难^-^原文中的公式如下,CKER再唠叨一下吧。其实混合的基本原理是就将要分色的图像各象素的颜色以及背景颜色均按照RGB规则各自分离之后,根据-图像的RGB颜色分量*alpha+背景的RGB颜色分量*(1-alpha)-这样一个简单公式来混合之后,最后将混合得到的RGB分量重新合并。』
 公式如下:
(Rs Sr + Rd Dr, Gs Sg + Gd Dg, Bs Sb + Bd Db, As Sa + Ad Da)
OpenGL
按照上面的公式计算这两个象素的混色结果。小写的sr分别代表源象素和目标象素。大写的SD则是相应的混色因子。这些决定了您如何对这些象素混色。绝大多数情况下,各颜色通道的alpha混色值大小相同,这样对源象素就有 (As, As, As, As),目标象素则有1, 1, 1, 1) - (As, As, As, As)。上面的公式就成了下面的模样:
(Rs As + Rd (1 - As), Gs As + Gd (1 - As), Bs As + Bs (1 - As), As As + Ad (1 - As))
这个公式会生成透明/半透明的效果。

OpenGL中的混色

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

#include <windows.h>   // // Windows的头文件 
#include <stdio.h>    // 标准输入输出库头文件
#include <gl\gl.h>    // OpenGL32库的头文件
#include <gl\glu.h>   // GLu32库的头文件
#include <gl\glaux.h>   // GLaux库的头文件
HDC  hDC=NULL;  // 私有GDI设备描述表
HGLRC  hRC=NULL;  // 永久着色描述表
HWND  hWnd=NULL;  // 保存我们的窗口句柄
HINSTANCE hInstance;  // 保存程序的实例
bool keys[256];   // 用于键盘例程的数组
bool active=TRUE;   // 窗口的活动标志,缺省为TRUE
bool fullscreen=TRUE;   // 全屏标志缺省设定成全屏模式
bool light;    // 光源的开/关 // Lighting ON/OFF
bool    blend;    // Blending OFF/ON? ( 新增 ) 
bool lp;    // L键按下了么? 
bool fp;    // F 键按下了么?
bool bp;    // B 键按下了么? ( 新增 ) 
GLfloat xrot;    // X 旋转 
GLfloat yrot;    // Y 旋转
GLfloat xspeed;    // X 旋转速度
GLfloat yspeed;    // Y 旋转速度
GLfloat z=-5.0f;    // 深入屏幕的距离 
GLfloat LightAmbient[]=  { 0.5f };  // 环境光参数
GLfloat LightDiffuse[]= { 1.0f };  // 漫射光参数
GLfloat LightPosition[]= { 0.0f };  // 光源位置
GLuint filter;    // 滤波类型
GLuint texture[3];   // 3种纹理的储存空
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // WndProc定义
28 marzo

。。。。。。

前天香港气温29,湿度97%,怪不得楼道顶上的管道不停的滴水。估计南京的黄梅天也不会达到这样的湿度吧。
 
昨天突然在我笔记本的显示屏你发现了一只虫子,生龙活虎的爬着。刚开始还以为是自己用的Ubuntu系统桌面觉得还挺好玩,结果发现只要虫子停下来我手按一下屏幕它就又开始爬了,我的机器是T43可不是X41T,不带手写板功能。经多方确认确实是一只真实存在的虫子。把笔记本屏幕外科看了又看死活没想通它是从哪儿进去的。
 
今天中午爬起来打开笔记本发现那种可爱的虫子牺牲了,本还想庆幸终于不会收到骚扰了,结果很郁闷的看到那只牺牲的虫子居然停留在我电脑屏幕的正中央。真的是死了都不让我清静,抑或者是让我眼中只有它。
 
最后结论,香港的天气很无敌,香港的虫子更是无敌。
 
19 marzo

迷茫

前几天看到三星研发中心有个数字电视研发项目需要招3D图形开发的人,投了份简历过去,前天我爸告诉我有三星的人打电话找我,据说对我以前在飞利浦做的实习项目有兴趣。等我打过去的时候人家听说我不在南京,立刻就没啥好说的了。郁闷不已阿。
 
再看看现在的招聘信息,要JAVA开发的公司肯定都是J2EE,难道JAVA只能搞这个?J2SE和J2ME都没用的阿。倒是有两个搞JAVA应用程序开发的公司曾经联系过我,不过不在南京,放弃。除了JAVA就是C/C++了,我只是会用,精通肯定算不上,又断了一条找工作的门路。
 
N多的人教导我找工作要把自己说的想万金油一样,说什么会什么。可惜就是学不会这一招,用的时间最长的JAVA我也不能说精通阿,随便找个边边角角的问题肯定难住我,更别说要我现场写代码了,从本科开始一向都是一边查一边写的,让我把结构,思路写出来那肯定没问题。可惜这年头招聘就和中学考试一样什么都要背,其实知道怎么用,用的时候能查出来就好了唉。连具体的方法名都要记的一字不差,这个我肯定做不到每个都全对。也许我老了,记忆力不如那些小朋友了吧。
 
就要毕业了,工作啊工作。继续迷茫。。。。。。。。。
06 marzo

Jeff Molofee的OPENGL教程-飘动的旗

大家好!对那些想知道我在这里作了些什么的朋友,您可以先按文章的末尾所列出的链接,下载我那毫无意义的演示(Demo)看看先!我是bosco,我将尽我所能教您来实现一个以正弦波方式运动的图象。这一课基于NeHe的教程第六课,当然您至少也应该学会了一至六课的知识。您需要下载源码压缩包,并将压缩包内带的data目录连其下的位图一起释放至您的代码目录下。或者使用您自己的位图,当然它的尺寸必须适合OpenGL纹理的要求。
  在我们开始之前,先打开Visual C++(译者:我可是用的C++ Builder…)并在其他的#inlude之后,添加如下的代码。这将引入我们在程序中将要用到的复杂(译者:复杂吗?)数学函数sine和cosine。

  #include <math.h>                      // 引入数学函数库中的Sin()

  我们将使用points数组来存放网格各顶点独立的x,y,z坐标。这里网格由45×45点形成,换句话说也就是由44格×44格的小方格子依次组成了。wiggle_count用来指定纹理波浪的运动速度。每3帧一次看起来很不错,变量hold将存放一个用来对旗形波浪进行光滑的浮点数。这几行添加在程序头部,位于最后一行#include之后、GLuint texture[1]之前的位置。

  float points[ 45 ][ 45 ][3];                // Points网格顶点数组
  int wiggle_count = 0;                    // 指定旗形波浪的运动速度
  GLfloat hold;                        // 临时变量

  然后下移至LoadGLTextures()子过程。本课中使用的纹理文件名是Tim.bmp。找到LoadBMP("Data/NeHe.bmp")这一句,并用LoadBMP ("Data/Tim.bmp")替换它。

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

  接着在InitGL()函数的尾部return TRUE之前,添加如下的代码。

  glPolygonMode( GL_BACK, GL_FILL );             // 后表面完全填充
  glPolygonMode( GL_FRONT, GL_LINE );             // 前表面使用线条绘制

  上面的代码指定使用完全填充模式来填充多边形区域的背面(译者:或者叫做后表面吧)。相反,多边形的正面(译者:前表面)则使用轮廓线填充了。这些方式完全取决于您的个人喜好。并且与多边形的方位或者顶点的方向有关。详情请参考红宝书(Red Book)。这里我顺便推销一本推动我学习OpenGL的好书 — Addison-Wesley出版的《Programmer's Guide to OpenGL》。个人以为这是学习OpenGL的无价之宝。
  接着上面的代码并在return TRUE这一句之前,添加如下的几行。

  // 沿X平面循环
  for(int x=0; x<45; x++)
  {

      // 沿Y平面循环
      for(int y=0; y<45; y++)
      {

          // 向表面添加波浪效果
          points[x][y][0]=float((x/5.0f)-4.5f);
          points[x][y][1]=float((y/5.0f)-4.5f);
          points[x][y][2]=float(sin((((x/5.0f)*40.0f)/360.0f)*3.141592654*2.0f)
      }
  }


  这里感谢Graham Gibbons关于使用整数循环变量消除波浪间的脉冲锯齿的建议。
  上面的两个循环初始化网格上的点。使用整数循环可以消除由于浮点运算取整造成的脉冲锯齿的出现。我们将x和y变量都除以5,再减去4.5。这样使得我们的波浪可以“居中”(译者:这样计算所得结果将落在区间[-4.5,4.5]之间)。
  点[x][y][2]最后的值就是一个sine函数计算的结果。Sin()函数需要一个弧度参变量。将float_x乘以40.0f,得到角度值。然后除以360.0f再乘以PI,乘以2,就转换为弧度了。
  接着我将彻底重写DrawGLScene函数。

  int DrawGLScene(GLvoid)                 // 绘制我们的GL场景
  {
      int x, y;
                    // 循环变量
      float float_x, float_y, float_xb, float_yb;   // 用来将旗形的波浪分割成很小的四边形

  我们使用不同的变量来控制循环。下面的代码中大多数变量除了用来控制循环和存储临时变量之外并没有什么别的用处。

      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  //清除屏幕和深度缓冲
      glLoadIdentity();                   // 重置当前的模型观察矩阵

      glTranslatef(0.0f,0.0f,-12.0f);            // 移入屏幕12个单位

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

      glBindTexture(GL_TEXTURE_2D, texture[0]);       // 选择纹理

  正如您所见,上面的代码和第六课的很类似,唯一的区别就是我将场景挪的离镜头更远了一些。

      glBegin(GL_QUADS);                   // 四边形绘制开始
      for( x = 0; x < 44; x++ )               // 沿 X 平面 0-44 循环(45点)
      {
          for( y = 0; y < 44; y++ )
           // 沿 Y 平面 0-44 循环(45点)
          {

  接着开始使用循环进行多边形绘制。这里使用整型可以避免我以前所用的int()强制类型转换。

              float_x = float(x)/44.0f;       // 生成X浮点值
              float_y = float(y)/44.0f;       // 生成Y浮点值
              float_xb = float(x+1)/44.0f;      // X浮点值+0.0227f
              float_yb = float(y+1)/44.0f;      // Y浮点值+0.0227f

  上面我们使用4个变量来存放纹理坐标。每个多边形(网格之间的四边形)分别映射了纹理的1/44×1/44部分。循环首先确定左下顶点的值,然后我们据此得到其他三点的值。

              glTexCoord2f( float_x, float_y);    // 第一个纹理坐标 (左下角)
              glVertex3f( points[x][y][0], points[x][y][1], points[x][y][2] );

              glTexCoord2f( float_x, float_yb );
   // 第二个纹理坐标 (左上角)
              glVertex3f( points[x][y+1][0], points[x][y+1][1], points[x][y+1][2] )

              glTexCoord2f( float_xb, float_yb );
  // 第三个纹理坐标 (右上角)
              glVertex3f( points[x+1][y+1][0], points[x+1][y+1][1], points[x+1][y+1][2] );

              glTexCoord2f( float_xb, float_y );
   // 第四个纹理坐标 (右下角)
              glVertex3f( points[x+1][y][0], points[x+1][y][1], points[x+1][y][2] )
          }
      }
      glEnd();
                        // 四边形绘制结束

  上面几行使用glTexCoord2f()和glVertex3f()载入数据。提醒一点:四边形是逆时针绘制的。这就是说,您开始所见到的表面是背面。后表面完全填充了,前表面由线条组成。
  如果您按顺时针顺序绘制的话,您初始时见到的可能是前表面。也就是说您将看到网格型的纹理效果而不是完全填充的。

      if( wiggle_count == 2 )                // 用来降低波浪速度(每隔2帧一次)
      {

  每绘制两次场景,循环一次sine值,以产生运动效果。

          for( y = 0; y < 45; y++ )           // 沿Y平面循环
          {
              hold=points[0][y][2];
         // 存储当前左侧波浪值
              for( x = 0; x < 44; x++)        // 沿X平面循环
              {
                  // 当前波浪值等于其右侧的波浪值
                  points[x][y][2] = points[x+1][y][2];
              }
              points[44][y][2]=hold;
         // 刚才的值成为最左侧的波浪值
          }
          wiggle_count = 0;
               // 计数器清零
      }
      wiggle_count++;
                    // 计数器加一

  上面所作的事情是先存储每一行的第一个值,然后将波浪左移一下,是图象产生波浪。存储的数值挪到末端以产生一个永无尽头的波浪纹理效果。然后重置计数器wiggle_count以保持动画的进行。
  上面的代码由NeHe(2000年2月)修改过,以消除波浪间出现的细小锯齿。

      xrot+=0.3f;                       // X 轴旋转
      yrot+=0.2f;                      // Y 轴旋转
      zrot+=0.4f;                      // Z 轴旋转

      return TRUE;                      // 返回
  }

  标准的NeHe旋转增量。现在编译并运行程序,您将看到一个漂亮的位图波浪。除了嘘声一片之外,我不敢确信大家的反应。但我希望大家能从这一课中学到点什么。如果您有任何问题或者需要澄清的地方,请随便联络我。感谢大家。

Jeff Molofee的OPENGL教程-粗糙的世界

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

 一个sector(区)包含了一系列的多边形,所以下一个目标就是triangle(我们将只用三角形,这样写代码更容易些)。
 
typedef struct tagTRIANGLE     // 创建Triangle三角形结构
{
 VERTEX vertex[3];     // VERTEX矢量数组,大小为3
} TRIANGLE;       // 命名为 TRIANGLE
 
  三角形本质上是由一些(两个以上)顶点组成的多边形,顶点同时也是我们的最基本的分类单位。顶点包含了OpenGL真正感兴趣的数据。我们用3D空间中的坐标值(x,y,z)以及它们的纹理坐标(u,v)来定义三角形的每个顶点。
 
typedef struct tagVERTEX      // 创建Vertex顶点结构
{
 float x, y, z;      // 3D 坐标
 float u, v;       // 纹理坐标
} VERTEX;       // 命名为VERTEX
 

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

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

 
// 先前的定义: char* worldfile = "data\\world.txt";
void SetupWorld()       // 设置我们的世界
{
 FILE *filein;      // 工作文件
 filein = fopen(worldfile, "rt");     // 打开文件
 ...
 (读入数据资料)
 ...
 fclose(filein);      // 关闭文件
 return;       // 返回
}
张彩放 0573-6082032(H)
 

    下一个挑战是将每个单独的文本行读入变量。这有很多办法可以做到。一个问题是文件中并不是所有的行都包含有意义的信息。空行和注释不应该被读入。我们创建了一个叫做readstr()的函数。这个函数会从数据文件中读入一个有意义的行至一个已经初始化过的字符串。下面就是代码:
 
void readstr(FILE *f,char *string)     // 读入一个字符串
{
 do       // 循环开始
 {
  fgets(string, 255, f);    // 读入一行
 } while ((string[0] == '/') || (string[0] == '\n'));   // 考察是否有必要进行处理
 return;       // 返回
}
 

        下一步我们读入区段数据。这一课将只处理一个区段,不过实现一个多区段引擎也很容易。让我们将注意力转回SetupWorld()。程序必须知道区段内包含了多少个三角形。我们在数据文件中以下面这种形式定义三角形数量:
接下来是读取三角形数量的代码:
 
int numtriangles;       // 区段中的三角形数量
char oneline[255];       // 存储数据的字符串
...
readstr(filein,oneline);      // 读入一行数据
sscanf(oneline, "NUMPOLLIES %d\n", &numtriangles);   // 读入三角形数量
 
  余下的世界载入过程采用了相似的方法。接着,我们对区段进行初始化,并读入部分数据:
 
// 先前的定义: SECTOR sector1;
char oneline[255];       // 存储数据的字符串
int numtriangles;       // 区段的三角形数量
float x, y, z, u, v;       // 3D 和 纹理坐标
...
sector1.triangle = new TRIANGLE[numtriangles];   // numtriangles个三角形分配内存并设定指针
sector1.numtriangles = numtriangles;     // 定义区段1中的三角形数量
// 遍历区段中的每个三角形
for (int triloop = 0; triloop < numtriangles; triloop++)   // 遍历所有的三角形
{
 // 遍历三角形的每个顶点
 for (int vertloop = 0; vertloop < 3; vertloop++)   // 遍历所有的顶点
 {
  readstr(filein,oneline);    // 读入一行数据
  // 读入各自的顶点数据
  sscanf(oneline, "%f %f %f %f %f", &x, &y, &z, &u, &v);
  // 将顶点数据存入各自的顶点
  sector1.triangle[triloop].vertex[vertloop].x = x; // 区段 1,   triloop 个三角形,  vertloop 个顶点, x =x
  sector1.triangle[triloop].vertex[vertloop].y = y; // 区段 1,   triloop 个三角形,  vertloop 个顶点, y =y
  sector1.triangle[triloop].vertex[vertloop].z = z; // 区段 1,   triloop 个三角形,  vertloop 个顶点,   z =z
  sector1.triangle[triloop].vertex[vertloop].u = u; // 区段 1,   triloop 个三角形,  vertloop 个顶点,  u =u
  sector1.triangle[triloop].vertex[vertloop].v = v; // 区段 1,   triloop 个三角形,  vertloop 个顶点,  e=v
 }
}

 

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

这样实现起来就很简单.
下面从第一步开始吧(平移并旋转镜头)

 
if (keys[VK_RIGHT])       //方向键按下了么?
{
 yrot -= 1.5f;       // 向左旋转场景
}
if (keys[VK_LEFT])       // 左方向键按下了么?
{
 yrot += 1.5f;       // 向右侧旋转场景
}
if (keys[VK_UP])       // 向上方向键按下了么?
{
 xpos -= (float)sin(heading*piover180) * 0.05f;   // 沿游戏者所在的X平面移动
 zpos -= (float)cos(heading*piover180) * 0.05f;   // 沿游戏者所在的Z平面移动
 if (walkbiasangle >= 359.0f)     //  walkbiasangle>=359?
 {
  walkbiasangle = 0.0f;     //  walkbiasangle 设为l 0
 }
 else        // 否则
 {
   walkbiasangle+= 10;     // 如果 walkbiasangle < 359 ,则增加 10
 }
 walkbias = (float)sin(walkbiasangle * piover180)/20.0f;  // 使游戏者产生跳跃感
}
if (keys[VK_DOWN])      // 向下方向键按下了么?
{
 xpos += (float)sin(heading*piover180) * 0.05f;   // 沿游戏者所在的X平面移动
 zpos += (float)cos(heading*piover180) * 0.05f;   // 沿游戏者所在的Z平面移动
 if (walkbiasangle <= 1.0f)     //  walkbiasangle<=1?
 {
  walkbiasangle = 359.0f;    // 使 walkbiasangle 等于 359
 }
 else       // 否则
 {
  walkbiasangle-= 10;    // 如果 walkbiasangle > 1 减去 10
 }
 walkbias = (float)sin(walkbiasangle * piover180)/20.0f;  // 使游戏者产生跳跃感
}
 

    这个实现很简单。当左右方向键按下后,旋转变量yrot
相应增加或减少。当前后方向键按下后,我们使用sine和cosine函数重新生成镜头位置(您需要些许三角函数学的知识:-)。Piover180
是一个很简单的折算因子用来折算度和弧度。
   
接着您可能会问:walkbias是什么意思?这是NeHe的发明的单词:-)。基本上就是当人行走时头部产生上下摆动的幅度。我们使用简单的sine正弦波来调节镜头的Y轴位置。如果不添加这个而只是前后移动的话,程序看起来就没这么棒了。
    现在,我们已经有了下面这些变量。可以开始进行步骤2和3了。由于我们的程序还不太复杂,我们无需新建一个函数,而是直接在显示循环中完成这些步骤。
 
int DrawGLScene(GLvoid)      // 绘制 OpenGL 场景
{
 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清除 场景  深度缓冲
 glLoadIdentity();      // 重置当前矩阵
 GLfloat x_m, y_m, z_m, u_m, v_m;    // 顶点的临时 X, Y, Z, U  V 的数值
 GLfloat xtrans = -xpos;     // 用于游戏者沿X轴平移时的大小 
 GLfloat ztrans = -zpos;     // 用于游戏者沿Z轴平移时的大小
 GLfloat ytrans = -walkbias-0.25f;    // 用于头部的上下摆动
 GLfloat sceneroty = 360.0f - yrot;    // 位于游戏者方向的360度角
 int numtriangles;      // 保有三角形数量的整数
 glRotatef(lookupdown,1.0f,0,0);    // 上下旋转
 glRotatef(sceneroty,0,1.0f,0);    // 根据游戏者正面所对方向所作的旋转
  glTranslatef(xtrans, ytrans, ztrans);    // 以游戏者为中心的平移场景
 glBindTexture(GL_TEXTURE_2D, texture[filter]);  // 根据 filter 选择的纹理
  numtriangles = sector1.numtriangles;    // 取得Sector1的三角形数量
  // 逐个处理三角形
 for (int loop_m = 0; loop_m < numtriangles; loop_m++)  // 遍历所有的三角形
 {
  glBegin(GL_TRIANGLES);    // 开始绘制三角形
   glNormal3f( 0.0f, 0.0f, 1.0f);   // 指向前面的法线
   x_m = sector1.triangle[loop_m].vertex[0].x; // 第一点的 X 分量
   y_m = sector1.triangle[loop_m].vertex[0].y; // 第一点的 Y 分量
   z_m = sector1.triangle[loop_m].vertex[0].z; // 第一点的 Z 分量
   u_m = sector1.triangle[loop_m].vertex[0].u; // 第一点的 U  纹理坐标
   v_m = sector1.triangle[loop_m].vertex[0].v; // 第一点的 V  纹理坐标
   glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m); // 设置纹理坐标和顶点
  
x_m = sector1.triangle[loop_m].vertex[1].x; // 第二点的 X 分量 y_m = sector1.triangle[loop_m].vertex[1].y; // 第二点的 Y 分量 z_m = sector1.triangle[loop_m].vertex[1].z; // 第二点的 Z 分量 u_m = sector1.triangle[loop_m].vertex[1].u; // 第二点的 U 纹理坐标 v_m = sector1.triangle[loop_m].vertex[1].v; // 第二点的 V 纹理坐标 glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m); // 设置纹理坐标和顶点 x_m = sector1.triangle[loop_m].vertex[2].x; // 第三点的 X 分量 y_m = sector1.triangle[loop_m].vertex[2].y; // 第三点的 Y 分量 z_m = sector1.triangle[loop_m].vertex[2].z; // 第三点的 Z 分量 u_m = sector1.triangle[loop_m].vertex[2].u; // 第二点的 U 纹理坐标 v_m = sector1.triangle[loop_m].vertex[2].v; // 第二点的 V 纹理坐标 glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m); // 设置纹理坐标和顶点 glEnd(); // 三角形绘制结束 } return TRUE; // 返回 }

    搞定!我们已经完成了自己的第一帧画面。这绝对算不上什么Quake,但咳...,我们绝对也不是Carmack或者Abrash。运行程序时,您可以按下F、B、
PgUp 和 PgDown 键来看看效果。PgUp /
PgDown简单的上下倾斜镜头。如果NeHe决定保留的话,程序中使用的纹理取自于我的学校ID证件上的照片,并且做了浮雕效果....:)。

    现在您也许在考虑下一步该做什么。但还是不要考虑使用这些代码来实现完整的3D引擎,写这个程序的目的也并非如此。您也许希望您的游戏中不止存在一个Sector,尤其是实现类似入口这样的部分,您还可能需要使用多边形(超过3个顶点)。程序现在的代码实现允许载入多个Sector并剔除了背面(背向镜头不用绘制的多边形)。将来我会写个这样的教程,但这需要更多的数学知识基础。

Jeff Molofee的OPENGL教程-飘动的星星

欢迎进入第九课。到现在为止,您应该很好的理解OpenGL了。『CKER:如果没有的话,一定是我翻译的罪过......』。您已经学会了设置一个OpenGL窗口的每个细节。学会在旋转的物体上贴图并打上光线以及混色(透明)处理。这一课应该算是第一课中级教程。您将学到如下的知识:在3D场景中移动位图,并去除位图上的黑色象素(使用混色)。接着为黑白纹理上色,最后您将学会创建丰富的色彩,并把上过不同色彩的纹理相互混合,得到简单的动画效果。
我们在第一课的代码基础上进行修改。先在程序源码的开始处增加几个变量。出于清晰起见,我重写了整段代码。
#include <windows.h>     // Windows的头文件
#include <stdio.h>      // 标准输入输出库头文件
#include <gl\gl.h>      // OpenGL32库的头文件
#include <gl\glu.h>     // GLu32库的头文件
#include <gl\glaux.h>     // GLaux库的头文件
HDC  hDC=NULL;    // 私有GDI设备描述表
HGLRC  hRC=NULL;    // 永久着色描述表
HWND  hWnd=NULL;    // 保存我们的窗口句柄
HINSTANCE hInstance;    // 保存程序的实例
bool keys[256];     // 用于键盘例程的数组
bool active=TRUE;     // 窗口的活动标志,缺省为TRUE
bool fullscreen=TRUE;     // 全屏标志缺省设定成全屏模式
下列这几行新加的。twinkle tp是布尔变量, 表示它们只能设为 TRUE 或 FALSE。 twinkle用来跟踪 闪烁 效果是否启用。 tp用来检查 'T'键有没有被按下或松开. (按下时 tp=TRUE, 松开时 tp=FALSE).
BOOL twinkle;      // 
闪烁的星星 BOOL tp; // 'T' 按下了么?
num 跟踪屏幕上所绘制的星星数。这个数字被定义为一个常量。这意味着无法在以后的代码中对其进行修改。这么做的原因是因为您无法重新定义一个数组。因此,如果我们定义一个50颗星星的数组,然后又将num增加到51的话,就会出错『CKER:数组越界』。不过您还是可以(也只可以)在这一行上随意修改这个数字。但是以后请您别再改动 num 的值了,除非您想看见灾难发生。
const num=50;      //绘制的星星数

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

typedef struct      // 为星星创建一个结构
{
 int r, g, b;      // 星星的颜色
 GLfloat dist;     // 星星距离中心的距离
 GLfloat angle;     // 当前星星所处的角度
}
stars;       // 结构命名为stars
stars star[num];      // 使用 'stars' 结构生成一个包含 'num'个元素的 'star'数组
 
接下来我们设置几个跟踪变量:星星离观察者的距离变量(zoom),我们所见到的星星所处的角度(tilt),以及使闪烁的星星绕Z轴自转的变量spin
loop变量用来绘制50颗星星。texture[1]用来存放一个黑白纹理。如果您需要更多的纹理的话,您应该增加texture数组的大小至您决定采用的纹理个数。
 
GLfloat zoom=-15.0f;     // 
星星离观察者的距离 GLfloat tilt=90.0f; // 星星的倾角 GLfloat spin; // 闪烁星星的自转 GLuint loop; // 全局l Loop 变量 GLuint texture[1]; // 存放一个纹理 LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // WndProc的声明
 
紧接着上面的代码就是我们用来载入纹理的代码。我不打算再详细的解释这段代码。这跟我们在第六、七、八课中所用的代码是一模一样的。这一次载入的位图叫做star.bmp。这里我们使用glGenTextures(1, &texture[0]),来生成一个纹理。纹理采用线性滤波方式。
 
AUX_RGBImageRec *LoadBMP(char *Filename)  // 载入位图文件
{
 FILE *File=NULL;     // 文件句柄
 if (!Filename)     // 确认已给出文件名
 {
  return NULL;    // 若无返回 NULL
 }
 File=fopen(Filename,"r");    // 检查文件是否存在
 if (File)      // 文件存在么?
 {
  fclose(File);    // 关闭文件句柄
  return auxDIBImageLoad(Filename);  // 载入位图并返回指针
 }
 return NULL;     // 如果载入失败返回 NULL
}
 
下面的代码(调用上面的代码)载入位图,并转换成纹理。变量用来跟踪纹理是否已载入并创建好了。
 
int LoadGLTextures()     // 载入位图并转换成纹理
{
 int Status=FALSE;    // Status 状态指示器
 AUX_RGBImageRec *TextureImage[1];  // 为纹理分配存储空间
 memset(TextureImage,0,sizeof(void *)*1);  // 将指针设为 NULL
 // 载入位图,查错,如果未找到位图文件则退出
 if (TextureImage[0]=LoadBMP("Data/Star.bmp"))
 {
  Status=TRUE;    // 将 Status 设为TRUE
  glGenTextures(1, &texture[0]);  // 创建一个纹理
  // 创建一个线性滤波纹理
  glBindTexture(GL_TEXTURE_2D, texture[0]);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
  glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 
  0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
 }
 if (TextureImage[0])    // 如果纹理存在
 {
  if (TextureImage[0]->data)   // 如果纹理图像存在
  {
   free(TextureImage[0]->data); // 释放纹理图像所占的内存
  }
  free(TextureImage[0]);   // 释放图像结构
 }
 return Status;     // 返回 Statu
s的值 }
 
现在设置OpenGL的渲染方式。这里不打算使用深度测试,如果您使用第一课的代码的话,请确认是否已经去掉了 glDepthFunc(GL_LEQUAL); 和 glEnable(GL_DEPTH_TEST);两行。否则,您所见到的效果将会一团糟。这里我们使用了纹理映射,因此请您确认您已经加上了这些第一课中所没有的代码。您会注意到我们通过混色来启用了纹理映射。
 
int InitGL(GLvoid)      // 此处开始对OpenGL进行所有设置
{
 if (!LoadGLTextures())    // 调用纹理载入子例程
 {
  return FALSE;    // 如果未能载入,返回FALSE
 }
 glEnable(GL_TEXTURE_2D);   // 启用纹理映射
 glShadeModel(GL_SMOOTH);   // 启用阴影平滑 
 glClearColor(0.0f, 0.0f, 0.0f, 0.5f);   // 黑色背景
 glClearDepth(1.0f);    // 设置深度缓存
 glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // 真正精细的透视修正
 glBlendFunc(GL_SRC_ALPHA,GL_ONE);  // 设置混色函数取得半透明效果
 glEnable(GL_BLEND);    // 启用混色
 
以下是新增的代码。设置了每颗星星的起始角度、距离、和颜色。您会注意到修改结构的属性有多容易。全部50颗星星都会被循环设置。要改变star[1]的角度我们所要做的只是star[1].angle={某个数值};就这么简单!
 
 for (loop=0; loop<num; loop++)   // 创建循环设置全部星星
 {
  star[loop].angle=0.0f;   // 所有星星都从零角度开始
 
loop颗星星离中心的距离是将loop的值除以星星的总颗数,然后乘上5.0f。基本上这样使得后一颗星星比前一颗星星离中心更远一点。这样当loop为50时(最后一颗星星),loop 除以 num正好是1.0f。之所以要乘以5.0f是因为1.0f*5.0f 就是 5.0f。『CKER:废话,废话!这老外怎么跟孔乙己似的!:)』5.0f已经很接近屏幕边缘。我不想星星飞出屏幕,5.0f是最好的选择了。当然如果如果您将场景设置的更深入屏幕里面的话,也许可以使用大于5.0f的数值,但星星看起来就更小一些(都是透视的缘故)。
您还会注意到每颗星星的颜色都是从0~255之间的一个随机数。也许您会奇怪为何这里的颜色得取值范围不是OpenGL通常的0.0f~1.0f之间。这里我们使用的颜色设置函数是glColor4ub,而不是以前的glColor4f。ub意味着参数是Unsigned Byte型的。一个byte的取值范围是0~255。这里使用byte值取随机整数似乎要比取一个浮点的随机数更容易一些。
 
  star[loop].dist=(float(loop)/num)*5.0f;  // 计算星星离中心的距离
  star[loop].r=rand()%256;   // star[loop]设置随机红色分量
  star[loop].g=rand()%256;   // star[loop]设置随机红色分量
  star[loop].b=rand()%256;   // star[loop]设置随机红色分量
 }
 return TRUE;     // 初始化一切OK
}
 
Resize的代码也是一样的,现在我们转入绘图代码。如果您使用第一课的代码,删除旧的DrawGLScene代码,只需将下面的代码复制过去就行了。实际上,第一课的代码只有两行,所以没太多东西要删掉的。
 
int DrawGLScene(GLvoid)     // 此过程中包括所有的绘制代码
{
 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清除屏幕及深度缓存
 glBindTexture(GL_TEXTURE_2D, texture[0]);  // 
选择纹理 for (loop=0; loop<num; loop++) // 循环设置所有的星星 { glLoadIdentity(); // 绘制每颗星星之前,重置模型观察矩阵 glTranslatef(0.0f,0.0f,zoom); // 深入屏幕里面 (使用 'zoom'的值) glRotatef(tilt,1.0f,0.0f,0.0f); // 倾斜视角(使用 'tilt'的值)
 
现在我们来移动星星。星星开始时位于屏幕的中心。我们要做的第一件事是把场景沿Y轴旋转。如果我们旋转90度的话,X轴不再是自左至右的了,他将由里向外穿出屏幕。为了让大家更清楚些,举个例子。假想您站在房子中间。再设想您左侧的墙上写着-x,前面的墙上写着-z,右面墙上就是+x咯,您身后的墙上则是+z。加入整个房子向右转90度,但您没有动,那么前面的墙上将是-x而不再是-z了。所有其他的墙也都跟着移动。-z出现在右侧,+z出现在左侧,+x出现在您背后。神经错乱了吧?通过旋转场景,我们改变了x和z平面的方向。
第二行代码沿x轴移动一个正值。通常x轴上的正值代表移向了屏幕的右侧(也就是通常的x轴的正向),但这里由于我们绕y轴旋转了坐标系,x轴的正向可以是任意方向。如果我们转180度的话,屏幕的左右侧就镜像反向了。因此,当我们沿
x轴正向移动时,可能向左,向右,向前或向后。
 
  glRotatef(star[loop].angle,0.0f,1.0f,0.0f); //旋转至当前所画星星的角度
  glTranslatef(star[loop].dist,0.0f,0.0f);  // 沿X轴正向移动
 
接着的代码带点小技巧。星星实际上是一个平面的纹理。现在您在屏幕中心画了个平面的四边形然后贴上纹理,这看起来很不错。一切都如您所想的那样。但是当您当您沿着y轴转上个90度的话,纹理在屏幕上就只剩右侧和左侧的两条边朝着您。看起来就是一条细线。这不是我们所想要的。我们希望星星永远正面朝着我们,而不管屏幕如何旋转或倾斜。
我们通过在绘制星星之前,抵消对星星所作的任何旋转来实现这个愿望。您可以采用逆序来抵消旋转。当我们倾斜屏幕时,我们实际上以当前角度旋转了星星。通过逆序,我们又以当前角度"反旋转"星星。也就是以当前角度的负值来旋转星星。就是说,如果我们将星星旋转了10度的话,又将其旋转-10度来使星星在那个轴上重新面对屏幕。下面的第一行抵消了沿y轴的旋转。然后,我们还需要抵消掉沿x轴的屏幕倾斜。要做到这一点,我们只需要将屏幕再旋转-tilt倾角。在抵消掉x和y轴的旋转后,星星又完全面对着我们了。
 
  glRotatef(-star[loop].angle,0.0f,1.0f,0.0f); // 取消当前星星的角度
  glRotatef(-tilt,1.0f,0.0f,0.0f);   // 取消屏幕倾斜
 
如果 twinkle  TRUE,我们在屏幕上先画一次不旋转的星星:将星星总数(num) 减去当前的星星数(loop)再减去1,来提取每颗星星的不同颜色(这么做是因为循环范围从0到num-1)。举例来说,结果为10的时候,我们就使用10号星星的颜色。这样相邻星星的颜色总是不同的。这不是个好法子,但很有效。最后一个值是alpha通道分量。这个值越小,这颗星星就越暗。
由于启用了twinkle,每颗星星最后会被绘制两遍。程序运行起来会慢一些,这要看您的机器性能如何了。但两遍绘制的星星颜色相互融合,会产生很棒的效果。同时由于第一遍的星星没有旋转,启用twinkle后的星星看起来有一种动画效果。(如果您这里看不懂得话,就自己去看程序的运行效果吧。)
值得注意的是给纹理上色是件很容易的事。尽管纹理本身是黑白的,纹理将变成我们在绘制它之前选定的任意颜色。此外,同样值得注意的是我们在这里使用的颜色值是byte型的,而不是通常的浮点数。甚至alpha通道分量也是如此。
 
  if (twinkle)     // 启用闪烁效果
  {
   // 使用byte型数值指定一个颜色
   glColor4ub(star[(num-loop)-1].r,star[(num-loop)-1].g,star[(num-loop)-1].b,255);
   glBegin(GL_QUADS);   // 开始绘制纹理映射过的四边形
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f,-1.0f, 0.0f);
    glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f,-1.0f, 0.0f);
    glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 0.0f);
    glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 0.0f);
   glEnd();     // 四边形绘制结束
  }
 
  现在绘制第二遍的星星。唯一和前面的代码不同的是这一遍的星星肯定会被绘制,并且这次的星星绕着z轴旋转。
 
  glRotatef(spin,0.0f,0.0f,1.0f);   // 绕z轴旋转星星
  //  使用byte型数值指定一个颜色
  glColor4ub(star[loop].r,star[loop].g,star[loop].b,255);
  glBegin(GL_QUADS);    // 开始绘制纹理映射过的四边形
   glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f,-1.0f, 0.0f);
   glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f,-1.0f, 0.0f);
   glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 0.0f);
   glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 0.0f);
  glEnd();      // 四边形绘制结束
 
  以下的代码代表星星的运动。我们增加spin的值来旋转所有的星星(公转)。然后,将每颗星星的自转角度增加loop/num。这使离中心更远的星星转的更快。最后减少每颗星星离屏幕中心的距离。这样看起来,星星们好像被不断地吸入屏幕的中心。  
 
  spin+=0.01f;     // 星星的公转
  star[loop].angle+=float(loop)/num;   // 改变星星的自转角度
  star[loop].dist-=0.01f;    // 改变星星离中心的距离
 
  接着几行检查星星是否已经碰到了屏幕中心。当星星碰到屏幕中心时,我们为它赋一个新颜色,然后往外移5个单位,这颗星星将踏上它回归屏幕中心的旅程。  
 
  if (star[loop].dist<0.0f)   // 星星到达中心了么
  {
   star[loop].dist+=5.0f;  // 往外移5个单位
   star[loop].r=rand()%256;  // 赋一个新红色分量
   star[loop].g=rand()%256;  // 赋一个新绿色分量
   star[loop].b=rand()%256;  // 赋一个新蓝色分量
  }
 }
 return TRUE;     // 一切正常
}
 
  现在我们添加监视键盘的代码。下移到WinMain()。找到SwapBuffers(hDC)一行。我们就在这一行后面增加键盘监视代码。
代码将检查T键是否已按下。如果T键按下过,并且又放开了,if块内的代码将被执行。如果twinkle为FALSE,他将变为TRUE。反之亦然。只要T键按下, tp就变为TRUE。这样处理可以防止如果您一直按着T键的话,块内的代码被反复执行。
  SwapBuffers(hDC);   // 切换缓冲 (双缓冲)
  if (keys['T'] && !tp)    // 是否T 键已按下并且 tp值为 FALSE
  {
   tp=TRUE;   // 若是,将tp设为TRUE
   twinkle=!twinkle;   // 翻转 twinkle的值
  }
下面的代码检查是否松开了T键。若是,使 tp=FALSE。除非tp的值为FALSE,否则按着T键时什么也不会发生。所以这行代码很重要。
  if (!keys['T'])    //  T 键已松开了么?
  {
   tp=FALSE;   // 若是 tp FALSE
  }
 
余下的代码检查上、下方向键,向上翻页键或向下翻页键是否按下。
  if (keys[VK_UP])    // 上方向键按下了么?
  {
   tilt-=0.5f;    // 屏幕向上倾斜
  }
  if (keys[VK_DOWN])   // 下方向键按下了么?
  {
   tilt+=0.5f;    // 屏幕向下倾斜
  }
  if (keys[VK_PRIOR])   // 向上翻页键按下了么
  {
   zoom-=0.2f;   // 缩小
  }
  if (keys[VK_NEXT])   // 向下翻页键按下了么?
  {
   zoom+=0.2f;   // 放大
  }
 
像以前一样,确认窗口的标题是否正确。
  if (keys[VK_F1])    // 
F1键按下了么? { keys[VK_F1]=FALSE; // 若是,使对应的Key数组中的值为 FALSE KillGLWindow(); // 销毁当前的窗口 fullscreen=!fullscreen; // 切换 全屏 / 窗口 模式 // 重建 OpenGL 窗口 if (!CreateGLWindow("NeHe's Textures, Lighting & Keyboard Tutorial",640,480,16,fullscreen)) { return 0; // 如果窗口未能创建,程序退出 } } } }
 
这一课我尽我所能来解释如何加载一个灰阶位图纹理,(使用混色)去掉它的背景色后,再给它上色,最后让它在3D场景中移动。我已经向您展示了如何创建漂亮的颜色与动画效果。实现原理是在原始位图上再重叠一份位图拷贝。到现在为止,只要您很好的理解了我所教您的一切,您应该已经能够毫无问题的制作您自己的3D Demo了。所有的基础知识都已包括在内!
然后往下移动到 LoadGLTextures() 这里。找到" if (TextureImage[0]=LoadBMP("Data/Crate.bmp")) "这一行。我们现在使用有色玻璃纹理来代替上一课中的木箱纹理。
 if (TextureImage[0]=LoadBMP("Data/glass.bmp")); // 载入玻璃位图 ( 已修改 )
InitGL()代码段加入以下两行。第一行以全亮度绘制此物体,并对其进行50%alpha混合(半透明)。当混合选项打开时,此物体将会产生50%的透明效果。第二行设置所采用的混合类型。
Rui Martins 的补充: alpha通道的值为 0.0意味着物体材质是完全透明的。1.0 则意味着完全不透明。
 glColor4f(1.0f,1.0f,1.0f,0.5f);   // 全亮度, 50% Alpha 混合( 新增 ) 
 glBlendFunc(GL_SRC_ALPHA,GL_ONE);  // 基于源象素alpha通道值的半透明混合函数 ( 新增 )
在接近第七课结尾处的地方找到下面的代码段。
 if (keys[VK_LEFT])    //Left方向键按下了么? 
 {
  yspeed-=0.01f;    //若是, 减少yspeed 
 }
接着上面的代码,我们增加如下的代码。这几行监视B键是否按下。如果是的话,计算机检查混合选项是否已经打开。然后将其置为相反的状态。
 if (keys['B'] && !bp)    // B 健按下且bp FALSE?
 {
  bp=TRUE;    // 若是, bp 设为 TRUE
  blend = !blend;    // 切换混合选项的 TRUE / FALSE 
  if(blend)    // 混合打开了么?
  {
   glEnable(GL_BLEND);  // 打开混合
   glDisable(GL_DEPTH_TEST); // 关闭深度测试
  }
  else     // 否则
  {
   glDisable(GL_BLEND);  // 关闭混合
   glEnable(GL_DEPTH_TEST); // 打开深度测试
  }
 }
 if (!keys['B'])     //  B 键松开了么?
 {
  bp=FALSE;    // 若是, bp设为 FALSE
 }

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

原文注 (11/13/99)

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

纹理贴图的Alpha混合

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

Jeff Molofee的OPENGL教程-纹理滤波方式,光源

这一课我会教您如何使用三种不同的纹理滤波方式。教您如何使用键盘来移动场景中的对象,还会教您在OpenGL场景中应用简单的光照。这一课包含了很多内容,如果您对前面的课程有疑问的话,先回头复习一下。进入后面的代码之前,很好的理解基础知识十分重要。
我们还是在第一课的代码上加以修改。跟以前不一样的是,只要有任何大的改动,我都会写出整段代码。程序开始,我们先加上几个新的变量。
#include <windows.h> // Windows的头文件
#include <stdio.h>
// 标准输入/输出的头文件( 新增 )
#include <gl\gl.h>
// OpenGL32库的头文件
#include <gl\glu.h>
// GLu32库的头文件
#include <gl\glaux.h>
// GLaux库的头文件

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

bool keys[256];
// 用于键盘例程的数组
bool active=TRUE;
// 窗口的活动标志,缺省为TRUE
bool fullscreen=TRUE;
// 全屏标志缺省设定成全屏模式
下面几行是新的。我们增加三个布尔变量。 light 变量跟踪光照是否打开。变量lp和fp用来存储'L' 和'F'键是否按下的状态。后面我会解释这些变量的重要性。现在,先放在一边吧。
BOOL light; // 光源的开/关
BOOL lp;
// L键按下了么?
BOOL fp;
// F键按下了么?
现在设置5个变量来控制绕x轴和y轴旋转角度的步长,以及绕x轴和y轴的旋转速度。另外还创建了一个z变量来控制进入屏幕深处的距离。
GLfloat xrot; // X 旋转
GLfloat yrot;
// Y 旋转
GLfloat xspeed;
// X 旋转速度
GLfloat yspeed;
// Y 旋转速度

GLfloat z=-5.0f;
// 深入屏幕的距离
接着设置用来创建光源的数组。我们将使用两种不同的光。第一种称为环境光。环境光来自于四面八方。所有场景中的对象都处于环境光的照射中。第二种类型的光源叫做漫射光。漫射光由特定的光源产生,并在您的场景中的对象表面上产生反射。处于漫射光直接照射下的任何对象表面都变得很亮,而几乎未被照射到的区域就显得要暗一些。这样在我们所创建的木板箱的棱边上就会产生的很不错的阴影效果。
创建光源的过程和颜色的创建完全一致。前三个参数分别是RGB三色分量,最后一个是alpha通道参数。
因此,下面的代码我们得到的是半亮(0.5f)的白色环境光。如果没有环境光,未被漫射光照到的地方会变得十分黑暗。
GLfloat LightAmbient[]= { 0.5f, 0.5f, 0.5f, 1.0f }; //环境光参数 ( 新增 )
下一行代码我们生成最亮的漫射光。所有的参数值都取成最大值1.0f。它将照在我们木板箱的前面,看起来挺好。
GLfloat LightDiffuse[]= { 1.0f, 1.0f, 1.0f, 1.0f }; // 漫射光参数 ( 新增 )
最后我们保存光源的位置。前三个参数和glTranslate中的一样。依次分别是XYZ轴上的位移。由于我们想要光线直接照射在木箱的正面,所以XY轴上的位移都是0.0f。第三个值是Z轴上的位移。为了保证光线总在木箱的前面,所以我们将光源的位置朝着观察者(就是您哪。)挪出屏幕。我们通常将屏幕也就是显示器的屏幕玻璃所处的位置称作Z轴的0.0f点。所以Z轴上的位移最后定为2.0f。假如您能够看见光源的话,它就浮在您显示器的前方。当然,如果木箱不在显示器的屏幕玻璃后面的话,您也无法看见箱子。『译者注:我很欣赏NeHe的耐心。说真的有时我都打烦了,这么简单的事他这么废话干嘛?但如果什么都清楚,您还会翻着这样的页面看个没完么?』
最后一个参数取为1.0f。这将告诉OpenGL这里指定的坐标就是光源的位置,以后的教程中我会多加解释。
GLfloat LightPosition[]= { 0.0f, 0.0f, 2.0f, 1.0f }; // 光源位置 ( 新增 )
filter 变量跟踪显示时所采用的纹理类型。第一种纹理(texture 0) 使用gl_nearest(不光滑)滤波方式构建。第二种纹理 (texture 1) 使用gl_linear(线性滤波) 方式,离屏幕越近的图像看起来就越光滑。第三种纹理 (texture 2) 使用 mipmapped滤波方式,这将创建一个外观十分优秀的纹理。根据我们的使用类型,filter 变量的值分别等于 0, 1 或 2 。下面我们从第一种纹理开始。
GLuint texture[3] 为三种不同纹理分配储存空间。它们分别位于在 texture[0], texture[1] 和 texture[2]中。
GLuint filter; // 滤波类型
GLuint texture[3]; // 3种纹理的储存空间

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // WndProc定义
现在载入一个位图,并用它创建三种不同的纹理。这一课使用glaux辅助库来载入位图,因此在编译时您应该确认是否包含了glaux库。我知道DelphiVC++都包含了glaux库,但别的语言不能保证都有。『译者注:glauxOpenGL辅助库,根据OpenGL的跨平台特性,所有平台上的代码都应通用。但辅助库不是正式的OpenGL标准库,没有出现在所有的平台上。但正好在Win32平台上可用。呵呵,BCB当然也没问题了。』这里我只对新增的代码做注解。如果您对某行代码有疑问的话,请查看教程六。那一课很详细的解释了载入、创建纹理的内容。
在上一段代码后面及ReSizeGLScene()之前的位置,我们增加了下面的代码。这和第六课中载入位图的代码几乎相同。
AUX_RGBImageRec *LoadBMP(char *Filename) // 载入位图
{
FILE *File=NULL;
// 文件句柄

if (!Filename)
// 确认文件名已初始化
{
return NULL;
// 没有返回 NULL
}

File=fopen(Filename,"r");
// 检查文件是否存在

if (File)
// 存在么?
{
fclose(File);
// 关闭文件句柄
return auxDIBImageLoad(Filename);
// 载入位图并返回一个指针
}
return NULL;
// 载入失败返回 NULL
}
这段代码调用前面的代码载入位图,并将其转换成3个纹理。Status 变量跟踪纹理是否已载入并被创建了。
int LoadGLTextures() // 载入位图并转换成纹理
{
int Status=FALSE;
// Status 指示器

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

memset(TextureImage,0,sizeof(void *)*1);
// 将指针设为 NULL
现在载入位图并转换成纹理。TextureImage[0]=LoadBMP("Data/Crate.bmp")调用我们的LoadBMP()函数。Data目录下的Crate.bmp将被载入。如果一切正常,图像数据将存放在TextureImage[0]Status变量被设为TRUE,我们将开始创建纹理。
// 载入位图,检查有错,或位图不存在的话退出。
if (TextureImage[0]=LoadBMP("Data/Crate.bmp"))
{
Status=TRUE;
// Status 设为 TRUE
现在我们已经将图像数据载入TextureImage[0]。我们将用它来创建3个纹理。下面的行告诉OpenGL我们要创建三个纹理,它们将存放在texture[0], texture[1]texture[2] 中。
glGenTextures(3, &texture[0]); // 创建纹理
第六课中我们使用了线性滤波的纹理贴图。这需要机器有相当高的处理能力,但它们看起来很不错。这一课中,我们接着要创建的第一种纹理使用 GL_NEAREST方式。从原理上讲,这种方式没有真正进行滤波。它只占用很小的处理能力,看起来也很差。唯一的好处是这样我们的工程在很快和很慢的机器上都可以正常运行。
您会注意到我们在 MINMAG 时都采用了GL_NEAREST,你可以混合使用 GL_NEARESTGL_LINEAR。纹理看起来效果会好些,但我们更关心速度,所以全采用低质量贴图。MIN_FILTER在图像绘制时小于贴图的原始尺寸时采用。MAG_FILTER在图像绘制时大于贴图的原始尺寸时采用。
// 创建 Nearest 滤波贴图
glBindTexture(GL_TEXTURE_2D, texture[0]);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
  // ( 新增 )
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST); // ( 新增 )
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
下个纹理与第六课的相同,线性滤波。唯一的不同是这次放在了 texture[1]中。因为这是第二个纹理。如果放在 texture[0]中的话,他将覆盖前面创建的 GL_NEAREST纹理。
// 创建线性滤波纹理
glBindTexture(GL_TEXTURE_2D, texture[1]);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
下面是创建纹理的新方法。 Mipmapping!『译者注:这个词的中文我翻不出来,不过没关系。看完这一段,您就知道意思最重要。』您可能会注意到当图像在屏幕上变得很小的时候,很多细节将会丢失。刚才还很不错的图案变得很难看。当您告诉OpenGL创建一个 mipmapped的纹理后,OpenGL将尝试创建不同尺寸的高质量纹理。当您向屏幕绘制一个 mipmapped纹理的时候,OpenGL将选择它已经创建的外观最佳的纹理(带有更多细节)来绘制,而不仅仅是缩放原先的图像(这将导致细节丢失)。
我曾经说过有办法可以绕过OpenGL对纹理宽度和高度所加的限制——64、128、256,等等。办法就是 gluBuild2DMipmaps。据我的发现,您可以使用任意的位图来创建纹理。OpenGL将自动将它缩放到正常的大小。
因为是第三个纹理,我们将它存到texture[2]。这样本课中的三个纹理全都创建好了。
// 创建 MipMapped 纹理
glBindTexture(GL_TEXTURE_2D, texture[2]);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_NEAREST);
// ( 新增 )
下面一行生成 mipmapped 纹理。我们使用三种颜色(红,绿,蓝)来生成一个2D纹理。TextureImage[0]->sizeX 是位图宽度,extureImage[0]->sizeY 是位图高度,GL_RGB意味着我们依次使用RGB色彩。GL_UNSIGNED_BYTE 意味着纹理数据的单位是字节。TextureImage[0]->data指向我们创建纹理所用的位图。  
gluBuild2DMipmaps(GL_TEXTURE_2D, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data); ( 新增 )
}
现在释放用来存放位图数据的内存。我们先查看位图数据是否存放在 TextureImage[0] 中,如果有,删掉。然后释放位图结构以确保内存被释放。
if (TextureImage[0]) // 纹理是否存在
{
if (TextureImage[0]->data)
// 纹理图像是否存在
{
free(TextureImage[0]->data);
// 释放纹理图像占用的内存
}
free(TextureImage[0]);
// 释放图像结构
}
最后我们返回 status 变量。如果一切OK,status 变量的值为TRUE。否则为FALSE。
return Status; // 返回 Status 变量
}
接着应该载入纹理并初始化OpenGL设置了。InitGL函数的第一行使用上面的代码载入纹理。创建纹理之后,我们调用
glEnable(GL_TEXTURE_2D)启用2D纹理映射。阴影模式设为平滑阴影( smooth shading )。背景色设为黑色,我们启用深度测试,然后我们启用优化透视计算。
int InitGL(GLvoid)  // 此处开始对OpenGL进行所有设置
{
if (!LoadGLTextures())
// 跳转到纹理载入例程
{
return FALSE; 
// 如果不能载入纹理返回 FALSE
}

glEnable(GL_TEXTURE_2D);
// 启用纹理映射
glShadeModel(GL_SMOOTH);
// 启用阴影平滑
glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
// 黑色背景
glClearDepth(1.0f);
// 深度缓存设置
glEnable(GL_DEPTH_TEST);
// 启用深度测试
glDepthFunc(GL_LEQUAL);
// 所作的深度测试类型
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
// 高度优化的透视投影计算
现在开始设置光源。下面下面一行设置环境光的发光量,光源light1开始发光。这一课的开始处我们我们将环境光的发光量存放在LightAmbient数组中。现在我们就使用此数组(半亮度环境光)。
glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient); // 设置环境光
接下来我们设置漫射光的发光量。它存放在LightDiffuse数组中(全亮度白光)。
glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse); // 设置漫射光
然后设置光源的位置。位置存放在 LightPosition 数组中(正好位于木箱前面的中心,X-0.0f,Y-0.0f,Z方向移向观察者2个单位<位于屏幕外面>)。
glLightfv(GL_LIGHT1, GL_POSITION,LightPosition); // 光源位置
最后,我们启用一号光源。我们还没有启用GL_LIGHTING,所以您看不见任何光线。记住:只对光源进行设置、定位、甚至启用,光源都不会工作。除非我们启用GL_LIGHTING。
glEnable(GL_LIGHT1); // 启用一号光源
return TRUE;
// 初始化 OK
}
下一段代码绘制贴图立方体。我只对新增的代码进行注解。如果您对没有注解的代码有疑问,回头看看第六课。
int DrawGLScene(GLvoid) // 从这里开始进行所有的绘制
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 清除屏幕和深度缓存
glLoadIdentity();
// 重置当前的模型观察矩阵
下三行代码放置并旋转贴图立方体。glTranslatef(0.0f,0.0f,z)将立方体沿着Z轴移动Z单位。glRotatef(xrot,1.0f,0.0f,0.0f)将立方体绕X轴旋转xrot。glRotatef(yrot,0.0f,1.0f,0.0f)将立方体绕Y轴旋转yrot。
glTranslatef(0.0f,0.0f,z);  // 移入/移出屏幕 z 个单位
glRotatef(xrot,1.0f,0.0f,0.0f);
// 绕X轴旋转
glRotatef(yrot,0.0f,1.0f,0.0f); );
// 绕Y轴旋转
下一行与我们在第六课中的类似。有所不同的是,这次我们绑定的纹理是texture[filter],而不是上一课中的texture[0]。任何时候,我们按下F键,filter 的值就会增加。如果这个数值大于2,变量filter 将被重置为0。程序初始时,变量filter 的值也将设为0。使用变量filter 我们就可以选择三种纹理中的任意一种。
glBindTexture(GL_TEXTURE_2D, texture[filter]); // 选择由filter决定的纹理
glBegin(GL_QUADS); // 开始绘制四边形
glNormal3f是这一课的新东西。Normal就是法线的意思,所谓法线是指经过面(多边形)上的一点且垂直于这个面(多边形)的直线。使用光源的时候必须指定一条法线。法线告诉OpenGL这个多边形的朝向,并指明多边形的正面和背面。如果没有指定法线,什么怪事情都可能发生:不该照亮的面被照亮了,多边形的背面也被照亮....。对了,法线应该指向多边形的外侧。

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

// 前侧面
glNormal3f( 0.0f, 0.0f, 1.0f);
// 法线指向观察者
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);
// Point 1 (Front)
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
// Point 2 (Front)
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
// Point 3 (Front)
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
// Point 4 (Front)
// 后侧面
glNormal3f( 0.0f, 0.0f,-1.0f);
// 法线背向观察者
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
// Point 1 (Back)
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
// Point 2 (Back)
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);
// Point 3 (Back)
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
// Point 4 (Back)
// 顶面
glNormal3f( 0.0f, 1.0f, 0.0f);
// 法线向上
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
// Point 1 (Top)
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
// Point 2 (Top)
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
// Point 3 (Top)
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);
// Point 4 (Top)
// 底面
glNormal3f( 0.0f,-1.0f, 0.0f);
// 法线朝下
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
// Point 1 (Bottom)
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
// Point 2 (Bottom)
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
// Point 3 (Bottom)
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);
// Point 4 (Bottom)
// 右侧面
glNormal3f( 1.0f, 0.0f, 0.0f);
// 法线朝右
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
// Point 1 (Right)
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);
// Point 2 (Right)
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
// Point 3 (Right)
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
// Point 4 (Right)
// 左侧面
glNormal3f(-1.0f, 0.0f, 0.0f);
// 法线朝左
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
// Point 1 (Left)
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);
// Point 2 (Left)
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
// Point 3 (Left)
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
// Point 4 (Left)
glEnd();
// 四边形绘制结束
下两行代码将xot和yrot的旋转值分别增加xspeed和yspeed个单位。xspeed和yspeed的值越大,立方体转得就越快。
xrot+=xspeed; // xrot 增加 xspeed 单位
yrot+=yspeed;
// yrot 增加 yspeed 单位
return TRUE;
 
}
现在转入WinMain()主函数。我们将在这里增加开关光源、旋转木箱、切换过滤方式以及将木箱移近移远的控制代码。在接近WinMain()函数结束的地方你会看到SwapBuffers(hDC)这行代码。然后就在这一行后面添加如下的代码。
代码将检查L键是否按下过。如果L键已按下,但lp的值不是false的话,意味着L键还没有松开,这时什么都不会发生。
SwapBuffers(hDC); // 交换缓存 (双缓存)
if (keys['L'] && !lp)
// L 键已按下并且松开了?
{
如果lp的值是false的话,意味着L键还没按下,或者已经松开了,接着lp将被设为TRUE。同时检查这两个条件的原因是为了防止L键被按住后,这段代码被反复执行,并导致窗体不停闪烁。
lp设为true之后,计算机就知道L键按过了,我们则据此可以切换光源的开/关:布尔变量light控制光源的开关。
lp=TRUE; // lp 设为 TRUE
light=!light;
// 切换光源的 TRUE/FALSE
下面几行来检查光源是否应该打开,并根据light变量的值。
if (!light) // 如果没有光源
{
glDisable(GL_LIGHTING);
//禁用光源
}
else // Otherwise
{
glEnable(GL_LIGHTING);
//启用光源
}
}
下面的代码查看是否松开了"L"键。如果松开,变量lp将设为false。这意味着"L"键没有按下。如果不作此检查,光源第一次打开之后,就无法再关掉了。计算机会以为"L"键一直按着呢。
if (!keys['L']) //L键松开了么?
{
lp=FALSE;
// 若是,则将lp设为FALSE
}
然后对"F"键作相似的检查。如果有按下"F"键并且"F"键没有处于按着的状态或者它就从没有按下过,将变量fp设为true。这意味着这个键正被按着呢。接着将filter变量加一。如果filter变量大于2(因为这里我们的使用的数组是texture[3],大于2的纹理不存在),我们重置filter变量为0。
if (keys['F'] && !fp) // F键按下了么?
{
fp=TRUE;
// fp 设为 TRUE
filter+=1;
// filter的值加一
if (filter>2)
// 大于2了么?
{
filter=0;
// 若是重置为0
}
}
if (!keys['F'])
//F键放开了么?
{
fp=FALSE;
// 若是fp设为FALSE
}
这四行检查是否按下了PageUp键。若是的话,减少z变量的值。这样DrawGLScene函数中包含的glTranslatef(0.0f,0.0f,z)调用将使木箱离观察者更远一点。
if (keys[VK_PRIOR]) //PageUp按下了?
{
z-=0.02f;
// 若按下,将木箱移向屏幕内部。
}
接着四行检查PageDown键是否按下,若是的话,增加z变量的值。这样DrawGLScene函数中包含的glTranslatef(0.0f,0.0f,z)调用将使木箱向着观察者移近一点。
if (keys[VK_NEXT]) // PageDown按下了么?
{
z+=0.02f;
//若按下的话,将木箱移向观察者。
}
现在检查方向键。按下左右方向键xspeed相应减少或增加。按下上下方向键yspeed相应减少或增加。记住在以后的教程中如果xspeed、yspeed的值增加的话,立方体就转的更快。如果一直按着某个方向键,立方体会在那个方向上转的越快。
if (keys[VK_UP]) // Up方向键按下了么?
{
xspeed-=0.01f;
//若是,减少xspeed
}
if (keys[VK_DOWN])
//Down方向键按下了么?
{
xspeed+=0.01f;
//若是,增加xspeed
}
if (keys[VK_RIGHT])
//Right方向键按下了么?
{
yspeed+=0.01f;
//若是,增加yspeed
}
if (keys[VK_LEFT])
//Left方向键按下了么?
{
yspeed-=0.01f;
//若是, 减少yspeed
}
像前几课一样,我们最后还需要更正窗体的标题。
if (keys[VK_F1]) // F1按下了么?
{
keys[VK_F1]=FALSE;
//若是将其设为FALSE
KillGLWindow();
//销毁当前窗口
fullscreen=!fullscreen;
// 切换全屏/窗口模式
// 重建GL窗口
if (!CreateGLWindow("NeHe's Textures, Lighting & Keyboard Tutorial",640,480,16,fullscreen))
{
return 0;
// 若无法创建窗口,程序退出
}
}
}
}
}

// 关闭
KillGLWindow();
// 销毁窗口
return (msg.wParam);
// 退出程序
}
这一课完了之后,您应该学会创建和使用这三种不同的纹理映射过滤方式。并使用键盘和场景中的对象交互。最后,您应该学会在场景中应用简单的光源,使得场景看起来更逼真。

Jeff Molofee的OPENGL教程-纹理映射

学习 texture map 纹理映射(贴图)有很多好处。比方说您想让一颗导弹飞过屏幕。根据前几课的知识,我们最可行的办法可能是很多个多边形来构建导弹的轮廓并加上有趣的颜色。使用纹理映射,您可以使用真实的导弹图像并让它飞过屏幕。您觉得哪个更好看?照片还是一大堆三角形和四边形?使用纹理映射的好处还不止是更好看,而且您的程序运行会更快。导弹贴图可能只是一个飞过窗口的四边形。一个由多边形构建而来的导弹却很可能包括成百上千的多边形。很显然,贴图极大的节省了CPU时间。
现在我们在第一课的代码开始处增加五行新代码。新增的第一行是 #include <stdio.h> 。它允许我们对文件进行操作,为了在后面的代码中使用 fopen() ,我们增加了这一行。然后我们增加了三个新的浮点变量... xrot , yrot zrot 。这些变量用来使立方体绕X、Y、Z轴旋转。最后一行 GLuint texture[1] 为一个纹理分配存储空间。如果您需要不止一个的纹理,应该将数字1改成您所需要的数字。
#include <windows.h> // Windows的头文件
#include <stdio.h>
// 标准输入/输出库的头文件 ( 新增 )
#include <gl\gl.h>
// OpenGL32库的头文件
#include <gl\glu.h>
// GLu32库的头文件
#include <gl\glaux.h>
// GLaux库的头文件

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

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

GLfloat xrot; // X 旋转量 ( 新增 )
GLfloat yrot;
// Y 旋转量 ( 新增 )
GLfloat zrot;
// Z 旋转量 ( 新增 )

GLuint texture[1];
// 存储一个纹理 ( 新增 )  

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
// WndProc的定义

紧跟上面的代码在 ReSizeGLScene() 之前,我们增加了下面这一段代码。这段代码用来加载位图文件。如果文件不存在,返回 NULL 告知程序无法加载位图。在我开始解释这段代码之前,关于用作纹理的图像我想有几点十分重要,并且您必须明白。此图像的宽和高必须是2的n次方;宽度和高度最小必须是64象素;并且出于兼容性的原因,图像的宽度和高度不应超过256象素。如果您的原始素材的宽度和高度不是64,128,256象素的话,使用图像处理软件重新改变图像的大小。可以肯定有办法能绕过这些限制,但现在我们只需要用标准的纹理尺寸。

首先,我们创建一个文件句柄。句柄是个用来鉴别资源的数值,它使程序能够访问此资源。我们开始先将句柄设为 NULL
AUX_RGBImageRec *LoadBMP(char *Filename) // 载入位图图象
{
FILE *File=NULL;
// 文件句柄
接下来检查文件名是否已提供。因为 LoadBMP() 可以无参数调用,所以我们不得不检查一下。您可不想什么都没载入吧.....:)
if (!Filename) // 确保文件名已提供。
{
return NULL;
// 如果没提供,返回 NULL
}
接着检查文件是否存在。下面这一行尝试打开文件。
File=fopen(Filename,"r"); //尝试打开文件
如果我们能打开文件的话,很显然文件是存在的。使用 fclose(File) 关闭文件。 auxDIBImageLoad(Filename) 读取图象数据并将其返回。
if (File) // 文件存在么?
{
fclose(File);
// 关闭句柄
return auxDIBImageLoad(Filename);
//载入位图并返回指针
}
如果我们不能打开文件,我们将返回NULL。这意味着文件无法载入。程序在后面将检查文件是否已载入。如果没有,我们将退出程序并弹出错误消息。
return NULL; // 如果载入失败,返回 NULL
}
下一部分代码载入位图(调用上面的代码)并转换成纹理。
int LoadGLTextures() // 载入位图(调用上面的代码)并转换成纹理
{
然后设置一个叫做 Status 的变量。我们使用它来跟踪是否能够载入位图以及能否创建纹理。 Status 缺省设为 FALSE (表示没有载入或创建任何东东)。
int Status=FALSE; // Status 状态指示器
现在我们创建存储位图的图像记录。次记录包含位图的宽度、高度和数据。
AUX_RGBImageRec *TextureImage[1]; // 创建纹理的存储空间
清除图像记录,确保其内容为空。
memset(TextureImage,0,sizeof(void *)*1); // 将指针设为 NULL
现在载入位图,并将其转换为纹理。 TextureImage[0]=LoadBMP("Data/NeHe.bmp") 调用 LoadBMP() 的代码。载入 Data 目录下的 NeHe.bmp 位图文件。如果一切正常,图像数据将存放在 TextureImage[0] 中, Status 被设为 TRUE ,然后我们开始创建纹理。
// 载入位图,检查有无错误,如果位图没找到则退出。
if (TextureImage[0]=LoadBMP("Data/NeHe.bmp"))
{
Status=TRUE;
// 将 Status 设为 TRUE
现在使用中 TextureImage[0] 的数据创建纹理。第一行 glGenTextures(1, &texture[0]) 告诉OpenGL我们想生成一个纹理名字(如果您想载入多个纹理,加大数字)。值得注意的是,开始我们使用 GLuint texture[1] 来创建一个纹理的存储空间,您也许会认为第一个纹理就是存放在 &texture[1] 中的,但这是错的。正确的地址应该是 &texture[0] 。同样如果使用 GLuint texture[2] 的话,第二个纹理存放在 texture[1] 中。『译者注:学C的,在这里应该没有障碍,数组就是从零开始的嘛。』

第二行 glBindTexture(GL_TEXTURE_2D, texture[0]) 告诉OpenGL将纹理名字 texture[0] 绑定到纹理目标上。2D纹理只有高度(在 Y 轴上)和宽度(在 X 轴上)。主函数将纹理名字指派给纹理数据。本例中我们告知OpenGL, &texture[0] 处的内存已经可用。我们创建的纹理将存储在 &texture[0] 的 指向的内存区域。
glGenTextures(1, &texture[0]); // 创建纹理

// 使用来自位图数据生成 的典型纹理

glBindTexture(GL_TEXTURE_2D, texture[0]);
下来我们创建真正的纹理。下面一行告诉OpenGL此纹理是一个2D纹理 ( GL_TEXTURE_2D )。数字代表图像的详细程度,通常就由它为零去了。数字是数据的成分数。因为图像是由红色数据,绿色数据,蓝色数据三种组分组成。 TextureImage[0]->sizeX 是纹理的宽度。如果您知道宽度,您可以在这里填入,但计算机可以很容易的为您指出此值。 TextureImage[0]->sizey 是纹理的高度。数字是边框的值,一般就是零。 GL_RGB 告诉OpenGL图像数据由红、绿、蓝三色数据组成。
GL_UNSIGNED_BYTE 意味着组成图像的数据是无符号字节类型的。最后... TextureImage[0]->data 告诉OpenGL纹理数据的来源。此例中指向存放在 TextureImage[0] 记录中的数据。
// 生成纹理
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
下面的两行告诉OpenGL在显示图像时,当它比放大得原始的纹理大 ( GL_TEXTURE_MAG_FILTER )或缩小得比原始得纹理小( GL_TEXTURE_MIN_FILTER )时OpenGL采用的滤波方式。通常这两种情况下我都采用 GL_LINEAR 。这使得纹理从很远处到离屏幕很近时都平滑显示。使用 GL_LINEAR 需要CPU和显卡做更多的运算。如果您的机器很慢,您也许应该采用 GL_NEAREST 。过滤的纹理在放大的时候,看起来斑驳的很『译者注:马赛克啦』。您也可以结合这两种滤波方式。在近处时使用 GL_LINEAR ,远处时 GL_NEAREST
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); // 线形滤波
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
// 线形滤波
}
现在我们释放前面用来存放位图数据的内存。我们先查看位图数据是否存放在处。如果是的话,再查看数据是否已经存储。如果已经存储的话,删了它。接着再释放 TextureImage[0] 图像结构以保证所有的内存都能释放。
if (TextureImage[0]) // 纹理是否存在
{
if (TextureImage[0]->data)
// 纹理图像是否存在
{
free(TextureImage[0]->data);
// 释放纹理图像占用的内存
}

free(TextureImage[0]);
// 释放图像结构
}
最后返回状态变量。如果一切OK,变量 Status 的值为 TRUE 。否则为 FALSE
return Status; // 返回 Status
}
我只在 InitGL 中增加很少的几行代码。但为了方便您查看增加了哪几行,我这段代码全部重贴一遍。 if (!LoadGLTextures()) 这行代码调用上面讲的子例程载入位图并生成纹理。如果因为任何原因 LoadGLTextures() 调用失败,接着的一行返回FALSE。如果一切OK,并且纹理创建好了,我们启用2D纹理映射。如果您忘记启用的话,您的对象看起来永远都是纯白色,这一定不是什么好事。
int InitGL(GLvoid) // 此处开始对OpenGL进行所有设置
{
if (!LoadGLTextures())
// 调用纹理载入子例程( 新增 )
{
return FALSE;
// 如果未能载入,返回FALSE( 新增 )
}

glEnable(GL_TEXTURE_2D);
// 启用纹理映射( 新增 )
glShadeModel(GL_SMOOTH);
// 启用阴影平滑
glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
// 黑色背景
glClearDepth(1.0f);
// 设置深度缓存
glEnable(GL_DEPTH_TEST);
// 启用深度测试
glDepthFunc(GL_LEQUAL);
// 所作深度测试的类型
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
// 真正精细的透视修正
return TRUE;
// 初始化 OK
}
现在我们绘制贴图『译者注:其实贴图就是纹理映射。将术语换来换去不好,我想少打俩字。^_^』过的立方体。这段代码被狂注释了一把,应该很好懂。开始两行代码 glClear() glLoadIdentity() 是第一课中就有的代码。 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) 清除屏幕并设为我们在 InitGL() 中选定的颜色,本例中是黑色。深度缓存也被清除。模型观察矩阵也使用glLoadIdentity()重置。
int DrawGLScene(GLvoid) // 从这里开始进行所有的绘制
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 清除屏幕和深度缓存
glLoadIdentity();
// 重置当前的模型观察矩阵
glTranslatef(0.0f,0.0f,-5.0f);
// 移入屏幕 5 个单位
下面三行使立方体绕X、Y、Z轴旋转。旋转多少依赖于变量 xrot yrot zrot 的值。
glRotatef(xrot,1.0f,0.0f,0.0f); // 绕X轴旋转
glRotatef(yrot,0.0f,1.0f,0.0f);
// 绕Y轴旋转
glRotatef(zrot,0.0f,0.0f,1.0f);
// 绕Z轴旋转
下一行代码选择我们使用的纹理。如果您在您的场景中使用多个纹理,您应该使用来 glBindTexture(GL_TEXTURE_2D, texture[ 所使用纹理对应的数字 ]) 选择要绑定的纹理。当您想改变纹理时,应该绑定新的纹理。有一点值得指出的是,您不能 glBegin() glEnd() 之间绑定纹理,必须在 glBegin() 之前或 glEnd() 之后绑定。注意我们在后面是如何使用 glBindTexture 来指定和绑定纹理的。
glBindTexture(GL_TEXTURE_2D, texture[0]); // 选择纹理
为了将纹理正确的映射到四边形上,您必须将纹理的右上角映射到四边形的右上角,纹理的左上角映射到四边形的左上角,纹理的右下角映射到四边形的右下角,纹理的左下角映射到四边形的左下角。如果映射错误的话,图像显示时可能上下颠倒,侧向一边或者什么都不是。

glTexCoord2f 的第一个参数是X坐标。 0.0f 是纹理的左侧。 0.5f 是纹理的中点, 1.0f 是纹理的右侧。 glTexCoord2f 的第二个参数是Y坐标。 0.0f 是纹理的底部。 0.5f 是纹理的中点, 1.0f 是纹理的顶部。

所以纹理的左上坐标是 X:0.0f,Y:1.0f ,四边形的左上顶点是 X: -1.0f,Y:1.0f 。其余三点依此类推。

试着玩玩 glTexCoord2f 的X,Y坐标参数。把 1.0f 改为 0.5f 将只显示纹理的左半部分,把 0.0f 改为 0.5f 将只显示纹理的右半部分。
glBegin(GL_QUADS);
// 前面
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); // 纹理和四边形的左下
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
// 纹理和四边形的右下
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
// 纹理和四边形的右上
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
// 纹理和四边形的左上
// 后面
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
// 纹理和四边形的右下
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
// 纹理和四边形的右上
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);
// 纹理和四边形的左上
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
// 纹理和四边形的左下
// 顶面
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
// 纹理和四边形的左上
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
// 纹理和四边形的左下
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
// 纹理和四边形的右下
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); 
// 纹理和四边形的右上
// 底面
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
// 纹理和四边形的右上
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
// 纹理和四边形的左上
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
// 纹理和四边形的左下
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);
// 纹理和四边形的右下
// 右面
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
// 纹理和四边形的右下
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);
// 纹理和四边形的右上
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
// 纹理和四边形的左上
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
// 纹理和四边形的左下
// 左面
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
// 纹理和四边形的左下
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);
// 纹理和四边形的右下
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
// 纹理和四边形的右上
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
// 纹理和四边形的左上
glEnd();
现在增加 xrot , yrot zrot 的值。尝试变化每次各变量的改变值来调节立方体的旋转速度,或改变+/-号来调节立方体的旋转方向。
xrot+=0.3f; // X 轴旋转
yrot+=0.2f;
// Y 轴旋转
zrot+=0.4f;
// Z 轴旋转
return true;
// 继续运行
}

现在您应该比较好的理解纹理映射(贴图)了。您应该掌握了给任意四边形表面贴上您所喜爱的图像的技术。一旦您对2D纹理映射的理解感到自信的时候,试试给立方体的六个面贴上不同的纹理。

当您理解纹理坐标的概念后,纹理映射并不难理解。!

如果您有什么意见或建议请给我EMAIL。如果您认为有什么不对或可以改进,请告诉我。

Jeff Molofee的OPENGL教程-向3D进军

在上节课的内容上作些扩展,我们现在开始生成真正的3D对象,而不是象前两节课中那样3D世界中的2D对象。我们给三角形增加一个左侧面,一个右侧面,一个后侧面来生成一个金字塔(四棱锥)。给正方形增加左、右、上、下及背面生成一个立方体。

我们混合金字塔上的颜色,创建一个平滑着色的对象。给立方体的每一面则来个不同的颜色。
int DrawGLScene(GLvoid) // 此过程中包括所有的绘制代码
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 清除屏幕及深度缓存
glLoadIdentity();
// 重置模型观察矩阵
glTranslatef(-1.5f,0.0f,-6.0f); // 左移 1.5 单位,并移入屏幕 6.0

glRotatef(rtri,0.0f,1.0f,0.0f);
// 绕Y轴旋转金字塔

glBegin(GL_TRIANGLES); // 开始绘制金字塔的各个面
有些人可能早已在上节课中的代码上尝试自行创建3D对象了。但经常有人来信问我:"我的对象怎么不会绕着其自身的轴旋转?看起来总是在满屏乱转。"要让您的对象绕自身的轴旋转,您必须让对象的中心坐标总是(0.0f,0,0f,0,0f)。
下面的代码创建一个绕者其中心轴旋转的金字塔。金字塔的上顶点离中心一个单位,底面离中心也是一个单位。上顶点在底面的投影位于底面的中心。

注意所有的面-三角形都是逆时针次序绘制的。这点十分重要,在以后的课程中我会作出解释。现在,您只需明白要么都逆时针,要么都顺时针,但永远不要将两种次序混在一起,除非您有足够的理由必须这么做。

我们开始画金字塔的前侧面。因为所有的面都共享上顶点,我们将这点在所有的三角形中都设置为红色。底边上的两个顶点的颜色则是互斥的。前侧面的左下顶点是绿色的,右下顶点是蓝色的。这样相邻右侧面的左下顶点是蓝色的,右下顶点是绿色的。
这样四边形的底面上的点的颜色都是间隔排列的。
glColor3f(1.0f,0.0f,0.0f); // 红色
glVertex3f( 0.0f, 1.0f, 0.0f);
// 三角形的上顶点 (前侧面)
glColor3f(0.0f,1.0f,0.0f);
// 绿色
glVertex3f(-1.0f,-1.0f, 1.0f);
// 三角形的左下顶点 (前侧面)
glColor3f(0.0f,0.0f,1.0f);
// 蓝色
glVertex3f( 1.0f,-1.0f, 1.0f);
// 三角形的右下顶点 (前侧面)
现在绘制右侧面。注意其底边上的两个顶点的X坐标位于中心右侧的一个单位处。顶点则位于Y轴上的一单位处,且Z坐标正好处于底边的两顶点的Z坐标中心。右侧面从上顶点开始向外侧倾斜至底边上。
这次的左下顶点用蓝色绘制,以保持与前侧面的右下顶点的一致。蓝色将从这个角向金字塔的前侧面和右侧面扩展并与其他颜色混合。
还应注意到后面的三个侧面和前侧面处于同一个glBegin(GL_TRIANGLES) 和 glEnd()语句中间。因为我们是通过三角形来构造这个金字塔的。OpenGL知道每三个点构成一个三角形。当它画完一个三角形之后,如果还有余下的点出现,它就以为新的三角形要开始绘制了。OpenGL在这里并不会将四点画成一个四边形,而是假定新的三角形开始了。所以千万不要无意中增加任何多余的点。
glColor3f(1.0f,0.0f,0.0f); // 红色
glVertex3f( 0.0f, 1.0f, 0.0f);
// 三角形的上顶点 (右侧面)
glColor3f(0.0f,0.0f,1.0f);
// 蓝色
glVertex3f( 1.0f,-1.0f, 1.0f);
// 三角形的左下顶点 (右侧面)
glColor3f(0.0f,1.0f,0.0f);
// 绿色
glVertex3f( 1.0f,-1.0f, -1.0f);
// 三角形的右下顶点 (右侧面)
现在是后侧面。再次切换颜色。左下顶点又回到绿色,因为后侧面与右侧面共享这个角。
glColor3f(1.0f,0.0f,0.0f); // 红色
glVertex3f( 0.0f, 1.0f, 0.0f);
// 三角形的上顶点 (后侧面)
glColor3f(0.0f,1.0f,0.0f);
// 绿色
glVertex3f( 1.0f,-1.0f, -1.0f);
// 三角形的左下顶点 (后侧面)
glColor3f(0.0f,0.0f,1.0f);
// 蓝色
glVertex3f(-1.0f,-1.0f, -1.0f);
// 三角形的右下顶点 (后侧面)
最后画左侧面。又要切换颜色。左下顶点是蓝色,与后侧面的右下顶点相同。右下顶点是蓝色,与前侧面的左下顶点相同。
到这里金字塔就画完了。因为金字塔只绕着Y轴旋转,我们永远都看不见底面,因而没有必要添加底面。如果您觉得有经验了,尝试增加底面(正方形),并将金字塔绕X轴旋转来看看您是否作对了。确保底面四个顶点的颜色与侧面的颜色相匹配。
glColor3f(1.0f,0.0f,0.0f); // 红色
glVertex3f( 0.0f, 1.0f, 0.0f);
// 三角形的上顶点 (左侧面)
glColor3f(0.0f,0.0f,1.0f);
// 蓝色
glVertex3f(-1.0f,-1.0f,-1.0f);
// 三角形的左下顶点 (左侧面)
glColor3f(0.0f,1.0f,0.0f);
// 绿色
glVertex3f(-1.0f,-1.0f, 1.0f);
// 三角形的右下顶点 (左侧面)
glEnd();
file://金字塔绘制结束
接下来开始画立方体。他由六个四边形组成。所有的四边形都以逆时针次序绘制。就是说先画右上角,然后左上角、左下角、最后右下角。您也许认为画立方体的背面的时候这个次序看起来好像顺时针,但别忘了我们从立方体的背后看背面的时候,与您现在所想的正好相反。(译者注:您是从立方体的外面来观察立方体的)。
注意到这次我们将立方体移地更远离屏幕了。因为立方体的大小要比金字塔大,同样移入6个单位时,立方体看起来要大的多。这是透视的缘故。越远的对象看起来越小 :)
glLoadIdentity();
glTranslatef(1.5f,0.0f,-7.0f);
// 先右移再移入屏幕

glRotatef(rquad,1.0f,1.0f,1.0f);
// 在XYZ轴上旋转立方体

glBegin(GL_QUADS);
// 开始绘制立方体
先画立方体的顶面。从中心上移一单位,注意Y坐标始终为一单位,表示这个四边形与Z轴平行。先画右上顶点,向右一单位,再屏幕向里一单位。然后左上顶点,向左一单位,再屏幕向里一单位。然后是靠近观察者的左下和右下顶点。就是屏幕往外一单位。
glColor3f(0.0f,1.0f,0.0f); // 颜色改为蓝色
glVertex3f( 1.0f, 1.0f,-1.0f);
// 四边形的右上顶点 (顶面)
glVertex3f(-1.0f, 1.0f,-1.0f);
// 四边形的左上顶点 (顶面)
glVertex3f(-1.0f, 1.0f, 1.0f);
// 四边形的左下顶点 (顶面)
glVertex3f( 1.0f, 1.0f, 1.0f);
// 四边形的右下顶点 (顶面)
底面的画法和顶面十分类似。只是Y坐标变成了-1。如果我们从立方体的下面来看立方体的话,您会注意到右上角离观察者最近,因此我们先画离观察者最近的顶点。然后是左上顶点最后才是屏幕里面的左下和右下顶点。

如果您真的不在乎绘制多边形的次序(顺时针或者逆时针)的话,您可以直接拷贝顶面的代码,将Y坐标从1改成 -1,也能够工作。但一旦您进入象纹理映射这样的领域时,忽略绘制次序会导致十分怪异的结果。
glColor3f(1.0f,0.5f,0.0f); // 颜色改成橙色
glVertex3f( 1.0f,-1.0f, 1.0f);
// 四边形的右上顶点(底面)
glVertex3f(-1.0f,-1.0f, 1.0f);
// 四边形的左上顶点(底面)
glVertex3f(-1.0f,-1.0f,-1.0f);
// 四边形的左下顶点(底面)
glVertex3f( 1.0f,-1.0f,-1.0f);
// 四边形的右下顶点(底面)
接着画立方体的前面。保持Z坐标为一单位,前面正对着我们。
glColor3f(1.0f,0.0f,0.0f); // 颜色改成红色
glVertex3f( 1.0f, 1.0f, 1.0f);
// 四边形的右上顶点(前面)
glVertex3f(-1.0f, 1.0f, 1.0f);
// 四边形的左上顶点(前面)
glVertex3f(-1.0f,-1.0f, 1.0f);
// 四边形的左下顶点(前面)
glVertex3f( 1.0f,-1.0f, 1.0f);
// 四边形的右下顶点(前面)
立方体后面的绘制方法与前面类似。只是位于屏幕的里面。注意Z坐标现在保持 -1 不变。
glColor3f(1.0f,1.0f,0.0f); // 颜色改成黄色
glVertex3f( 1.0f,-1.0f,-1.0f);
// 四边形的右上顶点(后面)
glVertex3f(-1.0f,-1.0f,-1.0f);
// 四边形的左上顶点(后面)
glVertex3f(-1.0f, 1.0f,-1.0f);
// 四边形的左下顶点(后面)
glVertex3f( 1.0f, 1.0f,-1.0f);
// 四边形的右下顶点(后面)
还剩两个面就完成了。您会注意到总有一个坐标保持不变。这一次换成了X坐标。因为我们在画左侧面。
glColor3f(0.0f,0.0f,1.0f); // 颜色改成蓝色
glVertex3f(-1.0f, 1.0f, 1.0f);
// 四边形的右上顶点(左面)
glVertex3f(-1.0f, 1.0f,-1.0f);
// 四边形的左上顶点(左面)
glVertex3f(-1.0f,-1.0f,-1.0f);
// 四边形的左下顶点(左面)
glVertex3f(-1.0f,-1.0f, 1.0f);
// 四边形的右下顶点(左面)
立方体的最后一个面了。X坐标保持为一单位。逆时针绘制。您愿意的话,留着这个面不画也可以,这样就是一个盒子:)

或者您要是有兴趣可以改变立方体所有顶点的色彩值,象金字塔那样混合颜色。您会看见一个非常漂亮的彩色立方体,各种颜色在它的各个表面流淌。
glColor3f(1.0f,0.0f,1.0f); // 颜色改成紫罗兰色
glVertex3f( 1.0f, 1.0f,-1.0f);
// 四边形的右上顶点(右面)
glVertex3f( 1.0f, 1.0f, 1.0f);
// 四边形的左上顶点(右面)
glVertex3f( 1.0f,-1.0f, 1.0f);
// 四边形的左下顶点(右面)
glVertex3f( 1.0f,-1.0f,-1.0f);
// 四边形的右下顶点(右面)
glEnd();
// 立方体绘制结束

rtri+=0.2f; // 增加三角形的旋转变量(新增)
rquad-=0.15f; // 减少四边形的旋转变量(新增)
return TRUE; // 继续运行
}
这一课又结束了。到这里您应该已经较好的掌握了在3D空间创建对象的方法。必须将OpenGL屏幕想象成一张很大的画纸,后面还带着许多透明的层。差不多就是个由大量的点组成的立方体。这些点从左至右、从上至下、从前到后的布满了这个立方体。如果您能想象的出在屏幕的深度方向,应该在设计新3D对象时没有任何问题。

如果您对3D空间的理解很困难的话,千万不要灰心! 刚开始的时候,领会这些内容会很难。象立方体这样的对象是您练习的好例子。继续努力吧!如果您有什么意见或建议请给我EMAIL。如果您认为有什么不对或可以改进,请告诉我。我想做最好的OpenGL教程并对您的反馈感兴趣。

Jeff Molofee的OPENGL教程-转起来

上一课中我教给您三角形和四边形的着色。这一课我将教您如何将这些彩色对象绕着坐标轴旋转。
其实只需在上节课的代码上增加几行就可以了。下面我将整个例程重写一遍。方便您知道增加了什么,修改了什么。
我们增加两个变量来控制这两个对象的旋转。这两个变量加在程序的开始处其他变量的后面(
bool fullscreen=TRUE;下面的两行)。它们是浮点类型的变量,使得我们能够非常精确地旋转对象。浮点数包含小数位置,这意味着我们无需使用1、2、3...的角度。你会发现浮点数是OpenGL编程的基础。新变量中叫做 rtri 的用来旋转三角形, rquad 旋转四边形。
#include <windows.h>// Windows的头文件
#include <gl\gl.h>// OpenGL32库的头文件
#include <gl\glu.h>// GLu32库的头文件
#include <gl\glaux.h>// GLaux库的头文件

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

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

GLfloat rtri; // 用于三角形的角度 ( 新增 )
GLfloat rquad;
// 用于四边形的角度 ( 新增 )
接着我们修改DrawGLScene()的代码。
下面这段代码与上一课的相同。
int DrawGLScene(GLvoid) // 此过程中包括所有的绘制代码
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 清除屏幕及深度缓存
glLoadIdentity();
// 重置模型观察矩阵
glTranslatef(-1.5f,0.0f,-6.0f); // 左移 1.5 单位,并移入屏幕 6.0
下一行代码是新的。glRotatef(Angle,Xvector,Yvector,Zvector)负责让对象绕某个轴旋转。这个命令有很多用处。 Angle 通常是个变量代表对象转过的角度。 Xvector , Yvector Zvector 三个参数则共同决定旋转轴的方向。比如(1,0,0)所描述的矢量经过X坐标轴的1个单位处并且方向向右。(-1,0,0)所描述的矢量经过X坐标轴的1个单位处,但方向向左。
D. Michael Traub:提供了对 Xvector , Yvector Zvector 的上述解释。
为了更好的理解X, Y 和 Z的旋转,我举些例子...

X轴-您正在使用一台台锯。锯片中心的轴从左至右摆放(就像OpenGL中的X轴)。尖利的锯齿绕着X轴狂转,看起来要么向上转,要么向下转。取决于锯片开始转时的方向。这与我们在OpenGL中绕着X轴旋转什么的情形是一样的。(译者注:这会儿您要把脸蛋凑向显示器的话,保准被锯开了花 ^-^。)

Y轴-假设您正处于一个巨大的龙卷风中心,龙卷风的中心从地面指向天空(就像OpenGL中的Y轴)。垃圾和碎片围着Y轴从左向右或是从右向左狂转不止。这与我们在OpenGL中绕着Y轴旋转什么的情形是一样的。

Z轴-您从正前方看着一台风扇。风扇的中心正好朝着您(就像OpenGL中的Z轴)。风扇的叶片绕着Z轴顺时针或逆时针狂转。这与我们在OpenGL中绕着Z轴旋转什么的情形是一样的。

下面的一行代码中,如果rtri等于7,我们将三角形绕着Y轴从左向右旋转7 。您也可以改变参数的值,让三角形绕着X和Y轴同时旋转。
glRotatef(rtri,0.0f,1.0f,0.0f); // 绕Y轴旋转三角形 ( 新增 )
下面的代码没有变化。在屏幕的左面画了一个彩色渐变三角形,并绕着Y轴从左向右旋转。
glBegin(GL_TRIANGLES); // 绘制三角形
glColor3f(1.0f,0.0f,0.0f); //设置当前色为红色
glVertex3f( 0.0f, 1.0f, 0.0f); // 上顶点
glColor3f(0.0f,1.0f,0.0f);//设置当前色为绿色
glVertex3f(-1.0f,-1.0f, 0.0f); // 左下
glColor3f(0.0f,0.0f,1.0f);//设置当前色为蓝色
glVertex3f( 1.0f,-1.0f, 0.0f); // 右下
glEnd(); // 三角形绘制结束
您会注意下面的代码中我们增加了另一个glLoadIdentity()调用。目的是为了重置模型观察矩阵。如果我们没有重置,直接调用glTranslate的话,会出现意料之外的结果。因为坐标轴已经旋转了,很可能没有朝着您所希望的方向。所以我们本来想要左右移动对象的,就可能变成上下移动了,取决于您将坐标轴旋转了多少角度。试试将glLoadIdentity() 注释掉之后,会出现什么结果。

重置模型观察矩阵之后,X,Y,Z轴都以复位,我们调用glTranslate。您会注意到这次我们只向右一了1.5单位,而不是上节课的3.0单位。因为我们重置场景的时候,焦点又回到了场景的中心(0.0处)。这样就只需向右移1.5单位就够了。
当我们移到新位置后,绕X轴旋转四边形。正方形将上下转动。
glLoadIdentity(); // 重置模型观察矩阵
glTranslatef(1.5f,0.0f,-6.0f); // 右移1.5单位,并移入屏幕 6.0
glRotatef(rquad,1.0f,0.0f,0.0f); // 绕X轴旋转四边形 ( 新增 )
下一段代码保持不变。在屏幕的右侧画一个蓝色的正方形。
glColor3f(0.5f,0.5f,1.0f); // 一次性将当前色设置为蓝色
glBegin(GL_QUADS); // 绘制正方形
glVertex3f(-1.0f, 1.0f, 0.0f);
// 左上
glVertex3f( 1.0f, 1.0f, 0.0f); // 右上
glVertex3f( 1.0f,-1.0f, 0.0f); // 左下
glVertex3f(-1.0f,-1.0f, 0.0f); // 右下
glEnd(); // 正方形绘制结束
下两行是新增的。倘若把 rtri rquad 想象为容器,那么在程序的开始我们创建了容器( GLfloat rtri , 和 GLfloat rquad )。当容器创建之后,里面是空的。下面的第一行代码是向容器中添加0.2。因此每次当我们运行完前面的代码后,都会在这里使 rtri 容器中的值增长0.2。后面一行将 rquad 容器中的值减少0.15。同样每次当我们运行完前面的代码后,都会在这里使 rquad 容器中的值下跌0.15。下跌最终会导致对象旋转的方向和增长的方向相反。

尝试改变下面代码中的+和-,来体会对象旋转的方向是如何改变的
。并试着将0.2改成1.0。这个数字越大,物体就转的越快,这个数字越小,物体转的就越慢。
rtri+=0.2f; // 增加三角形的旋转变量(新增)
rquad-=0.15f;
// 减少四边形的旋转变量(新增)
return TRUE; // 继续运行
}
 最后换掉窗口模式下的标题内容。
if (keys[VK_F1]) //  F1键按下了么?
{
keys[VK_F1]=FALSE;
// 若是,使对应的Key数组中的值为 FALSE
KillGLWindow(); // 销毁当前的窗口
fullscreen=!fullscreen; // 切换 全屏 / 窗口 模式
// 重建 OpenGL 窗口(修改)
if (!CreateGLWindow("NeHe's Rotation Tutorial",640,480,16,fullscreen))
{
return 0;
// 如果窗口未能创建,程序退出
}
}
在这一课中,我试着尽量详细的解释如何让对象绕某个轴转动。改改代码,试着让对象绕着Z轴、X+Y轴或者所有三个轴来转动:)。如果您有什么意见或建议请给我EMAIL。如果您认为有什么不对或可以改进,请告诉我。我想做最好的OpenGL教程并对您的反馈感兴趣。

Jeff Molofee的OPENGL教程-着色

上一课中我教给您三角形和四边形的绘制方法。这一课我将教您给三角形和四边形添加2种不同类型的着色方法。使用Flat coloring(单调着色)给四边形涂上固定的一种颜色。使用Smooth coloring(平滑着色)将三角形的三个顶点的不同颜色混合在一起,创建漂亮的色彩混合。

继续在上节课的DrawGLScene例程上修改。下面将整个例程重写了一遍。如果您计划修改上节课的代码,只需用下面的代码覆盖原来的DrawGLScene()就可以了。
int DrawGLScene(GLvoid) // 此过程中包括所有的绘制代码
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 清除屏幕及深度缓存
glLoadIdentity();
// 重置模型观察矩阵
glTranslatef(-1.5f,0.0f,-6.0f); // 左移 1.5 单位,并移入屏幕 6.0
glBegin(GL_TRIANGLES); // 绘制三角形
如果您还记得上节课的内容,这段代码在屏幕的左半部分绘制三角形。下一行代码是我们第一次使用命令glColor3f(r,g,b)。括号中的三个参数依次是红、绿、蓝三色分量。取值范围可以从0,0f到1.0f。类似于以前所讲的清除屏幕背景命令。

我们将颜色设为红色(纯红色,无绿色,无蓝色)。接下来的一行代码设置三角形的第一个顶点(三角形的上顶点),并使用当前颜色(红色)来绘制。从现在开始所有的绘制的对象的颜色都是红色,直到我们将红色改变成别的什么颜色。
glColor3f(1.0f,0.0f,0.0f); //设置当前色为红色
glVertex3f( 0.0f, 1.0f, 0.0f); // 上顶点
第一个红色顶点已经设置完毕。接下来我们设置第二个绿色顶点。三角形的左下顶点被设为绿色。
glColor3f(0.0f,1.0f,0.0f);//设置当前色为绿色
glVertex3f(-1.0f,-1.0f, 0.0f); // 左下
现在设置第三个也就是最后一个顶点。开始绘制之前将颜色设为蓝色。这将是三角形的右下顶点。glEnd()出现后,三角形将被填充。但是因为每个顶点有不同的颜色,因此看起来颜色从每个角喷出,并刚好在三角形的中心汇合,三种颜色相互混合。这就是平滑着色。
glColor3f(0.0f,0.0f,1.0f);//设置当前色为蓝色
glVertex3f( 1.0f,-1.0f, 0.0f); // 右下
glEnd(); // 三角形绘制结束

glTranslatef(3.0f,0.0f,0.0f); // 右移3单位
现在我们绘制一个单调着色-蓝色的正方形。最重要的是要记住,设置当前色之后绘制的所有东东都是当前色的。以后您所创建的每个工程都要使用颜色。即便是在完全采用纹理贴图的时候,glColor3f仍旧可以用来调节纹理的色调。等等....,以后再说吧。

我们必须要做的事只需将颜色一次性的设为我们想采用的颜色(本例采用蓝色),然后绘制场景。每个顶点都是蓝色的,因为我们没有告诉OpenGL要改变顶点的颜色。最后的结果是.....全蓝色的正方形。再说一遍,顺时针绘制的正方形意味着我们所看见的是四边形的背面。
glColor3f(0.5f,0.5f,1.0f); // 一次性将当前色设置为蓝色
glBegin(GL_QUADS); // 绘制正方形
glVertex3f(-1.0f, 1.0f, 0.0f);
// 左上
glVertex3f( 1.0f, 1.0f, 0.0f); // 右上
glVertex3f( 1.0f,-1.0f, 0.0f); // 左下
glVertex3f(-1.0f,-1.0f, 0.0f); // 右下
glEnd(); // 正方形绘制结束
return TRUE; // 继续运行
}
 最后换掉窗口模式下的标题内容。
if (keys[VK_F1]) //  F1键按下了么?
{
keys[VK_F1]=FALSE;
// 若是,使对应的Key数组中的值为 FALSE
KillGLWindow(); // 销毁当前的窗口
fullscreen=!fullscreen; // 切换 全屏 / 窗口 模式
// 重建 OpenGL 窗口(修改)
if (!CreateGLWindow("NeHe's Color Tutorial",640,480,16,fullscreen))
{
return 0;
// 如果窗口未能创建,程序退出
}
}
在这一课中,我试着尽量详细的解释如何为您的OpenGL多边形添加单调和平滑的着色效果的步骤。改改代码中的红绿蓝分量值,看看最后y有什么样的结果。如果您有什么意见或建议请给我EMAIL。如果您认为有什么不对或可以改进,请告诉我。我想做最好的OpenGL教程并对您的反馈感兴趣。

Jeff Molofee的OPENGL教程-第一个三角形

第一课中,我教您如何创建一个OpenGL窗口。这一课中,我将教您如何创建三角形和四边形。我们讲使用来创建GL_TRIANGLES一个三角形,GL_QUADS来创建一个四边形。

在第一课代码的基础上,我们只需在DrawGLScene()过程中增加代码。下面我重写整个过程。如果您计划修改上节课的代码,只需用下面的代码覆盖原来的DrawGLScene()就可以了。
int DrawGLScene(GLvoid) // 此过程中包括所有的绘制代码
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 清除屏幕及深度缓存
glLoadIdentity();
// 重置视口
当您调用glLoadIdentity()之后,您实际上讲当前点移到了屏幕中心,X坐标轴从左至右,Y坐标轴从下至上,Z坐标轴从里至外。OpenGL屏幕中心的坐标值是X和Y轴上的0.0f点。中心左面的坐标值是负值,右面是正值。移向屏幕顶端是正值,移向屏幕底端是负值。移入屏幕深处是负值,移出屏幕则是正值。

glTranslatef(x, y, z)沿着 X, Y 和 Z 轴移动。根据前面的次序,下面的代码沿着X轴左移1.5个单位,Y轴不动(0.0f),最后移入屏幕6.0f个单位。注意在glTranslatef(x, y, z)中当您移动的时候,您并不是相对屏幕中心移动,而是相对与当前所在的屏幕位置
glTranslatef(-1.5f,0.0f,-6.0f); // 左移 1.5 单位,并移入屏幕 6.0
现在我们已经移到了屏幕的左半部分,并且将视图推入屏幕背后足够的距离以便我们可以看见全部的场景-创建三角形。glBegin(GL_TRIANGLES)的意思是开始绘制三角形,glEnd() 告诉OpenGL三角形已经创建好了。通常您会需要画3个顶点,可以使用GL_TRIANGLES。在绝大多数的显卡上,绘制三角形是相当快速的。如果要画四个顶点,使用GL_QUADS的话会更方便。但据我所知,绝大多数的显卡都使用三角形来为对象着色。最后,如果您想要画更多的顶点时,可以使用GL_POLYGON。

本节的简单示例中,我们只画一个三角形。如果要画第二个三角形的话,可以在这三点之后,再加三行代码(3点)。所有六点代码都应包含在glBegin(GL_TRIANGLES) 和 glEnd()之间。在他们之间再不会有多余的点出现,也就是说,(GL_TRIANGLES) 和 glEnd()之间的点都是以三点为一个集合的。这同样适用于四边形。如果您知道实在绘制四边形的话,您必须在第一个四点之后,再加上四点为一个集合的点组。另一方面,多边形可以由任意个顶点,(GL_POLYGON)不在乎glBegin(GL_TRIANGLES) 和 glEnd()之间有多少行代码。

glBegin之后的第一行设置了多边形的第一个顶点,glVertex 的第一个参数是X坐标,然后依次是Y坐标和Z坐标。第一个点是上顶点,然后是左下顶点和右下顶点。glEnd()告诉OpenGL没有其他点了。这样将显示一个填充的三角形。

{译者:这里要注意的是存在两种不同的坐标变换方式,glTranslatef(x, y, z)中的x, y, z是相对与您当前所在点的位移,但glVertex(x,y,z)是相对于glTranslatef(x, y, z)移动后的新原点的位移。因而这里可以认为glTranslate移动的是坐标原点,glVertex中的点是相对最新的坐标原点的坐标值。}
glBegin(GL_TRIANGLES); // 绘制三角形
glVertex3f( 0.0f, 1.0f, 0.0f);
// 上顶点
glVertex3f(-1.0f,-1.0f, 0.0f); // 左下
glVertex3f( 1.0f,-1.0f, 0.0f); // 右下
glEnd(); // 三角形绘制结束
在屏幕的左半部分画完三角形后,我们要移到右半部分来画正方形。为此要再次使用glTranslate。这次右移,所以X坐标值为正值。因为前面左移了1.5个单位,这次要先向右移回屏幕中心(1.5个单位),再向右移动1.5个单位。总共要向右移3.0个单位。
glTranslatef(3.0f,0.0f,0.0f); // 右移3单位
现在使用GL_QUADS绘制正方形。与绘制三角形的代码相类似,画四边形也很简单。唯一的区别是用GL_QUADS来替换了GL_TRIANGLES。并增加了一个点。我们使用顺时针次序来画正方形-左上-右上-右下-左下。采用顺时针绘制的是对象的后表面。这就是说我们所看见的是正方形的背面。逆时针画出来的正方形才是正面朝着我们的。现在这对您来说并不重要,但以后您必须知道。
glBegin(GL_QUADS); // 绘制正方形
glVertex3f(-1.0f, 1.0f, 0.0f);
// 左上
glVertex3f( 1.0f, 1.0f, 0.0f); // 右上
glVertex3f( 1.0f,-1.0f, 0.0f); // 左下
glVertex3f(-1.0f,-1.0f, 0.0f); // 右下
glEnd(); // 正方形绘制结束
return TRUE; // 继续运行
}
最后换掉窗口模式下的标题内容。
if (keys[VK_F1]) //  F1键按下了么?
{
keys[VK_F1]=FALSE;
// 若是,使对应的Key数组中的值为 FALSE
KillGLWindow();
// 销毁当前的窗口
fullscreen=!fullscreen;
// 切换 全屏 / 窗口 模式
// 重建 OpenGL 窗口(修改)
if (!CreateGLWindow("NeHe's First Polygon Tutorial",640,480,16,fullscreen))
{
return 0;
// 如果窗口未能创建,程序退出
}
}
Markus Knauer 注: 在 ("OpenGL Programming Guide: The Official Guide to Learning OpenGL, Release 1", J. Neider, T. Davis, M. Woo, Addison-Wesley, 1993)《OpenGL编程指南:OpenGL学习的官方指南,第一版》一书中清楚的解释了NeHe所指的在OpenGL中移动的单位概念:
"在OpenGL中真的有英寸和英里的区别吗?答案是一句话:没有。透视和其他的变换都是无单位的。如果您想要裁剪在1.0到20.0米,英寸、公里等等之间的平面,在OpenGL中您无法做到。唯一的法则是您必须使用一致的度量单位。"
在这一课中,我已试着尽量详细的解释与多边形绘制有关的步骤。并创建了一个绘制三角形和正方形的OpenGL程序。如果您有什么意见或建议请给我EMAIL。如果您认为有什么不对或可以改进,请告诉我。我想做最好的OpenGL教程并对您的反馈感兴趣。

Jeff Molofee的OPENGL教程-新手上路

NeHe的教程一共有30多课,内容翔实,并且不断更新。国内的网站实在应该向他们学习。令人惊讶的是,NeHe提供的例程源码几乎都有跨平台的不同编译版本,涉及从Visual C++、Borland C++、Visual Basic、MacOS X/GLUT、Linux/GLX、Code Warrior、Delphi、C++ Builder、MASM、ASM、MingW32 & Allegro以及Python等等的不同平台下的多种编译器。这在国内市场上的百元大书中似乎也未曾见到。
关于OpenGL,是最早由SGI开发的跨平台的工业标准的3D图形硬件的软件接口,与微软的Direct X所鼎立,不必我来多吹。
由于CKER只是业余水准,关于OpenGL的专用术语的翻译难免有错误和不妥之处,请多加指正。
我是在1024X768的分辨率下编排的格式。贴到CSDN上估计变形很大,实在不行我就自己做网站去啦!
另外,要想流畅的运行例程,您的爱机应该够劲,内存应该够大,还要支持3D硬件加速的显卡,TNT总该有吧  :).......

第一课的内容有些简单,但这是NeHe几乎所有的OpenGl例程的框架。他有太废话,但看看不会错的。
}


欢迎来到我的 OpenGL教程。我是个对 OpenGL充满激情的普通男孩! 我第一次听说 OpenGL是 3Dfx 发布 Voodoo1 卡的 OpenGL硬件加速驱动的时候。我立刻意识到 OpenGL是那种必须学习的东西。不幸的是当时很难从书本或网络上找到关于 OpenGL的讯息。我花了 N 个 小时来调试自己书写的代码,甚至在 IRC和 EMail 上花更多的时间来恳求别人帮忙。但我发现那 些懂得 OpenGL 高手们保留了他们的精华,对共享知识也不感兴趣。实在让人灰心 !

我创建这个网站的目的是为了帮助那些对 OpenGL有兴趣却又需要帮助的人。在我的每个教程中,我都会尽可能详细的来解释每一行代码的作用。我会努力让我的代码更简单(您无需学习 MFC代码)!就算您是个VC 、OPENGL的绝对新手也应该可以读通代码,并清楚的知道发生了什么。我的站点只是许多提供 OpenGL教程的站点中的一个。如果您是 OpenGL的高级程序员的话,我的站点可能太简单了,但如果您才开始的话,我想这个站点会教会您许多东西!

教程的这一节在2000年一月彻底重写了一遍。将会教您如何设置一个 OpenGL窗口。它可以只是一个窗口或是全屏幕的、可以任意 大小、任意色彩深度。此处的代码很稳定且很强大,您可以在您所有的OpenGL项目中使用。我所有的教程都将基于此节的代码!所有的错误都有被报告。所以应该没有内存泄漏,代码也很容易阅读和修改。感谢Fredric Echols对代码所做的修改!

现在就让我们直接从代码开始吧。第一件事是打开VC然后创建一个新工程。如果您不知道如何创建的话,您也许不该学习OpenGL,而应该先学学VC。文末可供下载的代码是VC6的。某些版本的VC需要将 bool 改成 BOOL , true 改成 TRUE , false 改成 FALSE ,请自行修改。我用VC4和VC5编译过这些代码,没有发现问题。
在您创建一个新的Win32程序(不是console控制台程序)后,您还需要链接OpenGL库文件。在VC中操作如下:Project > Settings,然后单击LINK标签。在"Object/Library Modules"选项中的开始处(在 kernel32.lib 前)增加 OpenGL32.lib GLu32.lib GLaux.lib 后单击OK按钮。现在可以开始写您的OpenGL程序了。

代码的前4行包括了我们使用的每个库文件的头文件。如下所示:
#include <windows.h>
#include <gl\gl.h>
#include <gl\glu.h>
#include <gl\glaux.h>
// Windows的头文件
// OpenGL32库的头文件
// GLu32库的头文件
// GLaux库的头文件
接下来您需要设置您计划在您的程序中使用的所有变量。本节中的例程将创建一个空的OpenGL窗口,因此我们暂时还无需设置大堆的变量。余下需要设置的变量不多,但十分重要。您将会在您以后所写的每一个OpenGL程序中用到它们。
第一行设置的变量是Rendering Context(着色描述表)。每一个OpenGL都被连接到一个着色描述表上。着色描述表将所有的OpenGL调用命令连接到Device Context(设备描述表)上。我将OpenGL的着色描述表定义为 hRC 。要让您的程序能够绘制窗口的话,还需要创建一个设备描述表,也就是第二行的内容。Windows的设备描述表被定义为 hDC 。DC将窗口连接到GDI(Graphics Device Interface图形设备接口)。而RC将OpenGL连接到DC。第三行的变量 hWnd 将保存由Windows给我们的窗口指派的句柄。最后,第四行为我们的程序创建了一个Instance(实例)。
HGLRC hRC=NULL;
HDC hDC=NULL;
HWND hWnd=NULL;
HINSTANCE hInstance;
// 永久着色描述表
// 私有GDI设备描述表
// 保存我们的窗口句柄
// 保存程序的实例
下面的第一行设置一个用来监控键盘动作的数组。有许多方法可以监控键盘的动作,但这里的方法很可靠,并且可以处理多个键同时按下的情况。

active 变量用来告知程序窗口是否处于最小化的状态。如果窗口已经最小化的话,我们可以做从暂停代码执行到退出程序的任何事情。我喜欢暂停程序。这样可以使得程序不用在后台保持运行。

fullscreen 变量的作用相当明显。如果我们的程序在全屏状态下运行, fullscreen 的值为TRUE,否则为FALSE。这个全局变量的设置十分重要,它让每个过程都知道程序是否运行在全屏状态下。
bool keys[256];
bool active=TRUE;
bool fullscreen=TRUE;
// 用于键盘例程的数组
// 窗口的活动标志,缺省为TRUE
// 全屏标志缺省设定成全屏模式
现在我们需要先定义WndProc()。必须这么做的原因是CreateGLWindow()有对WndProc()的引用,但WndProc()在CreateGLWindow()之后才出现。在C语言中,如果我们想要访问一个当前程序段之后的过程和程序段的话,必须在程序开始处先申明所要访问的程序段。所以下面的一行代码先行定义了WndProc(),使得CreateGLWindow()能够引用WndProc()。
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // WndProc的定义
 
下面的代码的作用是重新设置OpenGL场景的大小,而不管窗口的大小是否已经改变(假定您没有使用全屏模式)。甚至您无法改变窗口的大小时(例如您在全屏模式下),它至少仍将运行一次--在程序开始时设置我们的透视图。OpenGL场景的尺寸将被设置成它显示时所在窗口的大小。
GLvoid ReSizeGLScene(GLsizei width, GLsizei height)
{
if (height==0)
{
height=1;
}
glViewport(0, 0, width, height);
// 重置并初始化GL窗口大小

// 防止被零除

// 将Height设为1

// 重置当前的视口(Viewport)
下面几行为透视图设置屏幕。意味着越远的东西看起来越小。这么做创建了一个现实外观的场景。此处透视按照基于窗口宽度和高度的45度视角来计算。0.1f,100.0f是我们在场景中所能绘制深度的起点和终点。

glMatrixMode(GL_PROJECTION)指明接下来的两行代码将影响projection matrix(投影矩阵)。投影矩阵负责为我们的场景增加透视。 glLoadIdentity()近似于重置。它将所选的矩阵状态恢复成其原始状态。调用 glLoadIdentity()之后我们为场景设置透视图。
glMatrixMode(GL_MODELVIEW)指明任何新的变换将会影响 modelview matrix(模型观察矩阵)。模型观察矩阵中存放了我们的物体讯息。最后我们重置模型观察矩阵。如果您还不能理解这些术语的含义,请别着急。在以后的教程里,我会向大家解释。只要知道如果您想获得一个精彩的透视场景的话,必须这么做。
glMatrixMode(GL_PROJECTION);
glLoadIdentity();

// 计算窗口的外观比例
gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,0.1f,100.0f);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
// 选择投影矩阵
// 重置投影矩阵


// 选择模型观察矩阵
// 重置模型观察矩阵

 
接下的代码段中,我们将对OpenGL进行所有的设置。我们将设置清除屏幕所用的颜色,打开深度缓存,启用smooth shading(阴影平滑),等等。这个例程直到OpenGL窗口创建之后才会被调用。此过程将有返回值。但我们此处的初始化没那么复杂,现在还用不着担心这个返回值。
int InitGL(GLvoid)
{
// 此处开始对OpenGL进行所有设置
 
下一行启用smooth shading(阴影平滑)。阴影平滑通过多边形精细的混合色彩,并对外部光进行平滑。我将在另一个教程中更详细的解释阴影平滑。
glShadeModel(GL_SMOOTH); // 启用阴影平滑
下一行设置清除屏幕时所用的颜色。如果您对色彩的工作原理不清楚的话,我快速解释一下。色彩值的范围从0.0f到1.0f。0.0f代表最黑的情况,1.0f就是最亮的情况。glClearColor 后的第一个参数是Red Intensity(红色分量),第二个是绿色,第三个是蓝色。最大值也是1.0f,代表特定颜色分量的最亮情况。最后一个参数是Alpha值。当它用来清除屏幕的时候,我们不用关心第四个数字。现在让它为0.0f。我会用另一个教程来解释这个参数。

通过混合三种原色(红、绿、蓝),您可以得到不同的色彩。希望您在学校里学过这些。因此,当您使用glClearColor(0.0f,0.0f,1.0f,0.0f),您将用亮蓝色来清除屏幕。如果您用 glClearColor(0.5f,0.0f,0.0f,0.0f)的话,您将使用中红色来清除屏幕。不是最亮(1.0f),也不是最暗 (0.0f)。要得到白色背景,您应该将所有的颜色设成最亮(1.0f)。要黑色背景的话,您该将所有的颜色设为最暗(0.0f)。
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // 黑色背景
接下来的三行必须做的是关于depth buffer(深度缓存)的。将深度缓存设想为屏幕后面的层。深度缓存不断的对物体进入屏幕内部有多深进行跟踪。我们本节的程序其实没有真正使用深度缓存,但几乎所有在屏幕上显示3D场景OpenGL程序都使用深度缓存。它的排序决定那个物体先画。这样您就不会将一个圆形后面的正方形画到圆形上来。深度缓存是OpenGL十分重要的部分。
glClearDepth(1.0f);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
// 设置深度缓存
// 启用深度测试
// 所作深度测试的类型
接着告诉OpenGL我们希望进行最好的透视修正。这会十分轻微的影响性能。但使得透视图看起来好一点。
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // 真正精细的透视修正
最后,我们返回TRUE。如果我们希望检查初始化是否OK,我们可以查看返回的 TRUE或FALSE的值。如果有错误发生的话,您可以加上您自己的代码返回FALSE。目前,我们不管它。
return TRUE;
}
// 初始化 OK
 
下一段包括了所有的绘图代码。任何您所想在屏幕上显示的东东都将在此段代码中出现。以后的每个教程中我都会在例程的此处增加新的代码。如果您对OpenGL已经有所了解的话,您可以在 glLoadIdentity()调用之后,返回TRUE值之前,试着添加一些OpenGL代码来创建基本的形。如果您是OpenGL新手,等着我的下个教程。目前我们所作的全部就是将屏幕清除成我们前面所决定的颜色,清除深度缓存并且重置场景。我们仍没有绘制任何东东。

返回TRUE值告知我们的程序没有出现问题。如果您希望程序因为某些原因而中止运行,在返回TRUE值之前增加返回FALSE的代码告知我们的程序绘图代码出错。程序即将退出。
int DrawGLScene(GLvoid)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 
glLoadIdentity(); 
return TRUE; 
}
// 从这里开始进行所有的绘制

// 清除屏幕和深度缓存
// 重置当前的模型观察矩阵
// 一切 OK
 
下一段代码只在程序退出之前调用。KillGLWindow() 的作用是依次释放着色描述表,设备描述表和窗口句柄。我已经加入了许多错误检查。如果程序无法销毁窗口的任意部分,都会弹出带相应错误消息的讯息窗口,告诉您什么出错了。使您在您的代码中查错变得更容易些。
GLvoid KillGLWindow(GLvoid) 
{
// 正常销毁窗口
 
我们在KillGLWindow()中所作的第一件事是检查我们是否处于全屏模式。如果是,我们要切换回桌面。我们本应在禁用全屏模式前先销毁窗口,但在某些显卡上这么做可能会使得桌面崩溃。所以我们还是先禁用全屏模式。这将防止桌面出现崩溃,并在Nvidia和3dfx显卡上都工作的很好!
if (fullscreen) 
{
// 我们处于全屏模式吗?
 
我们使用ChangeDisplaySettings(NULL,0)回到原始桌面。将NULL作为第一个参数,0作为第二个参数传递强制Windows使用当前存放在注册表中的值(缺省的分辨率、色彩深度、刷新频率,等等)来有效的恢复我们的原始桌面。切换回桌面后,我们还要使得鼠标指针重新可见。
ChangeDisplaySettings(NULL,0); 
ShowCursor(TRUE); 
}
// 是的话,切换回桌面
// 显示鼠标指针

 
接下来的代码查看我们是否拥有着色描述表(hRC)。如果没有,程序将跳转至后面的代码查看是否拥有设备描述表。
if (hRC) 
{
// 我们拥有着色描述表吗?
 
如果存在着色描述表的话,下面的代码将查看我们能否释放它(将 hRChDC分开)。这里请注意我使用的的查错方法。基本上我只是让程序尝试释放着色描述表(通过调用wglMakeCurrent(NULL,NULL),然后我再查看释放是否成功。巧妙的将数行代码结合到了一行。
if (!wglMakeCurrent(NULL,NULL)) 
{
// 我们能否释放DC和RC描述表?
 
如果不能释放DC和RC描述表的话,MessageBox()将弹出错误消息,告知我们DC和RC无法被释放。NULL意味着消息窗口没有父窗口。其右的文字将在消息窗口上出现。"SHUTDOWN ERROR"出现在窗口的标题栏上。MB_OK的意思消息窗口上带有一个写着OK字样的按钮。
MB_ICONINFORMATION将在消息窗口中显示一个带圈的小写的i(看上去更正式一些)。 
MessageBox(NULL,"Release Of DC And RC Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
}
下一步我们试着删除着色描述表。如果不成功的话弹出错误消息。
if (!wglDeleteContext(hRC)) 
{
// 我们能否删除RC?
 
如果无法删除着色描述表的话,将弹出错误消息告知我们RC未能成功删除。然后hRC被设为NULL。
MessageBox(NULL,"Release Rendering Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
}
hRC=NULL;                                                                                                   
  // 将RC设为 NULL
}
现在我们查看是否存在设备描述表,如果有尝试释放它。如果不能释放设备描述表将弹出错误消息,然后hDC设为NULL。
if (hDC && !ReleaseDC(hWnd,hDC))                                                         // 我们能否释放 DC?
{
MessageBox(NULL,"Release Device Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
hDC=NULL;                                                                                                    
// 将 DC 设为 NULL
}
现在我们来查看是否存在窗口句柄,我们调用 DestroyWindow( hWnd )来尝试销毁窗口。如果不能的话弹出错误窗口,然后hWnd被设为NULL。
if (hWnd && !DestroyWindow(hWnd))                                                        // 能否销毁窗口?
{
MessageBox(NULL,"Could Not Release hWnd.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
hWnd=NULL;                                                                                                 
// 将 hWnd 设为 NULL
}
最后要做的事是注销我们的窗口类。这允许我们正常销毁窗口,接着在打开其他窗口时,不会收到诸如"Windows Class already registered"(窗口类已注册)的错误消息。
if (!UnregisterClass("OpenGL",hInstance))                                                 // 能否注销类?
{
MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
hInstance=NULL;                                                                                         
// 将 hInstance 设为 NULL
}
}
接下来的代码段创建我们的OpenGL窗口。我花了很多时间来做决定是否创建固定的全屏模式这样不需要许多额外的代码,还是创建一个容易定制的友好的窗口但需要更多的代码。当然最后我选择了后者。我经常在EMail中收到诸如此类的问题:怎样创建窗口而不使用全屏幕?怎样改变窗口的标题栏?怎样改变窗口的分辨率或pixel format(象素格式)?以下的代码完成了所有这一切!尽管最好要学学材质,这会让您写自己的OpenGL程序变得容易的多!

正如您所见,此过程返回布尔变量(TRUE 或 FALSE)。他还带有5个参数:窗口的标题栏,窗口的宽度,窗口的高度,色彩位数(16/24/32),和全屏标志(TRUE --全屏模式, FALSE--窗口模式 )。返回的布尔值告诉我们窗口是否成功创建。
BOOL CreateGLWindow(char* title, int width, int height, int bits, bool fullscreenflag)
{
当我们要求Windows为我们寻找相匹配的象素格式时,Windows寻找结束后将模式值保存在变量PixelFormat中。
GLuint PixelFormat; // 保存查找匹配的结果
wc用来保存我们的窗口类的结构。窗口类结构中保存着我们的窗口信息。通过改变类的不同字段我们可以改变窗口的外观和行为。每个窗口都属于一个窗口类。当您创建窗口时,您必须为窗口注册类。
WNDCLASS wc; // 窗口类结构
dwExStyle和dwStyle存放扩展和通常的窗口风格信息。我使用变量来存放风格的目的是为了能够根据我需要创建的窗口类型(是全屏幕下的弹出窗口还是窗口模式下的带边框的普通窗口);来改变窗口的风格。
DWORD dwExStyle;
DWORD dwStyle;
// 扩展窗口风格
// 窗口风格
下面的5行代码取得矩形的左上角和右下角的坐标值。我们将使用这些值来调整我们的窗口使得其上的绘图区的大小恰好是我们所需的分辨率的值。通常如果我们创建一个640x480的窗口,窗口的边框会占掉一些分辨率的值。
RECT WindowRect;
WindowRect.left=(long)0;
WindowRect.right=(long)width;
WindowRect.top=(long)0;
WindowRect.bottom=(long)height;
// 取得矩形的左上角和右下角的坐标值
// 将Left   设为 0
// 将Right  设为要求的宽度
// 将Top    设为 0
// 将Bottom 设为要求的高度
下一行代码我们让全局变量fullscreen等于fullscreenflag。如果我们希望在全屏幕下运行而将fullscreenflag设为TRUE,但没有让变量fullscreen等于fullscreenflag的话,fullscreen变量将保持为FALSE。当我们在全屏幕模式下销毁窗口的时候,变量fullscreen的值却不是正确的TRUE值,计算机将误以为已经处于桌面模式而无法切换回桌面。上帝啊,但愿这一切都有意义。就是一句话,fullscreen的值必须永远fullscreenflag的值,否则就会有问题。{CKER也觉得此处太废话,懂的人都要不懂啦.....:(  }
fullscreen=fullscreenflag; // 设置全局全屏标志
下一部分的代码中,我们取得窗口的实例,然后定义窗口类。

CS_HREDRAW 和 CS_VREDRAW 的意思是无论何时,只要窗口发生变化时就强制重画。CS_OWNDC为窗口创建一个私有的DC。这意味着DC不能在程序间共享。WndProc是我们程序的消息处理过程。由于没有使用额外的窗口数据,后两个字段设为零。然后设置实例。接着我们将hIcon设为NULL,因为我们不想给窗口来个图标。鼠标指针设为标准的箭头。背景色无所谓(我们在GL中设置)。我们也不想要窗口菜单,所以将其设为NULL。类的名字可以您想要的任何名字。出于简单,我将使用"OpenGL"。
hInstance = GetModuleHandle(NULL);
wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
wc.lpfnWndProc = (WNDPROC) WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_WINLOGO);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = NULL;
wc.lpszMenuName = NULL;
wc.lpszClassName = "OpenGL";
// 取得我们窗口的实例
// 移动时重画,并为窗口取得DC
// WndProc处理消息
// 无额外窗口数据
// 无额外窗口数据
// 设置实例
// 装入缺省图标
// 装入鼠标指针
// GL不需要背景
// 不需要菜单
// 设定类名字
现在注册类名字。如果有错误发生,弹出错误消息窗口。按下上面的OK按钮后,程序退出。
if (!RegisterClass(&wc))                                    // 尝试注册窗口类
{
MessageBox(NULL,"Failed To Register The Window Class.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE;                                               file://退出并返回FALSE
}
查看程序应该在全屏模式还是窗口模式下运行。如果应该是全屏模式的话,我们将尝试设置全屏模式。
if (fullscreen)
{
// 要尝试全屏模式吗?
 
下一部分的代码看来很多人都会有问题要问关于.......切换到全屏模式。在切换到全屏模式时,有几件十分重要的事您必须牢记。必须确保您在全屏模式下所用的宽度和高度等同于窗口模式下的宽度和高度。最最重要的是要在创建窗口之前设置全屏模式。这里的代码中,您无需再担心宽度和高度,它们已被设置成与显示模式所对应的大小。
DEVMODE dmScreenSettings; 
memset(&dmScreenSettings,0,sizeof(dmScreenSettings)); 
dmScreenSettings.dmSize=sizeof(dmScreenSettings); 
dmScreenSettings.dmPelsWidth = width; 
dmScreenSettings.dmPelsHeight = height; 
dmScreenSettings.dmBitsPerPel = bits; 
dmScreenSettings.dmFields=DM_BITSPERPEL|DM_PELSWIDTH|DM_PELSHEIGHT;
// 设备模式
// 确保内存分配
// Devmode 结构的大小
// 所选屏幕宽度
// 所选屏幕高度
// 每象素所选的色彩深度
 
上面的代码中,我们分配了用于存储视频设置的空间。设定了屏幕的宽,高,色彩深度。下面的代码我们尝试设置全屏模式。我们在dmScreenSettings中保存了所有的宽,高,色彩深度讯息。下一行使用ChangeDisplaySettings来尝试切换成与dmScreenSettings所匹配模式。我使用参数CDS_FULLSCREEN来切换显示模式,因为这样做不仅移去了屏幕底部的状态条,而且它在来回切换时,没有移动或改变您在桌面上的窗口。
// 尝试设置显示模式并返回结果。注: CDS_FULLSCREEN 移去了状态条。
if (ChangeDisplaySettings(&dmScreenSettings,CDS_FULLSCREEN)!=DISP_CHANGE_SUCCESSFUL)
{
如果模式未能设置成功,我们将进入以下的代码。如果不能匹配全屏模式,弹出消息窗口,提供两个选项:在窗口模式下运行或退出。
// 若模式失败,提供两个选项:退出或在窗口内运行。
if (MessageBox(NULL,"The Requested Fullscreen Mode Is Not Supported By\nYour Video Card. Use Windowed Mode Instead?","NeHe GL",MB_YESNO|MB_ICONEXCLAMATION)==IDYES)
{
如果用户选择窗口模式,变量fullscreen 的值变为FALSE,程序继续运行。
fullscreen=FALSE;
}
else
{
// 选择窗口模式(Fullscreen=FALSE)


 
如果用户选择退出,弹出消息窗口告知用户程序将结束。并返回FALSE告诉程序窗口未能成功创建。程序退出。
// Pop Up A Message Box Letting User Know The Program Is Closing.
MessageBox(NULL,"Program Will Now Close.","ERROR",MB_OK|MB_ICONSTOP);
return FALSE;                                                               
//退出并返回 FALSE
}
}
}
由于全屏模式可能失败,用户可能决定在窗口下运行,我们需要在设置屏幕/窗口之前,再次检查fullscreen的值是TRUEFALSE。
if (fullscreen)
{
// 仍处于全屏模式吗?
 
如果我们仍处于全屏模式,设置扩展窗体风格为WS_EX_APPWINDOW,这将强制我们的窗体可见时处于最前面。再将窗体的风格设为WS_POPUP。这个类型的窗体没有边框,使我们的全屏模式得以完美显示。
最后我们禁用鼠标指针。当您的程序不是交互式的时候,在全屏模式下禁用鼠标指针通常是个好主意。
dwExStyle=WS_EX_APPWINDOW;
dwStyle=WS_POPUP;
ShowCursor(FALSE);
}
else
{
// 扩展窗体风格
// 窗体风格
// 隐藏鼠标指针


 
如果我们使用窗口而不是全屏模式,我们在扩展窗体风格中增加了 WS_EX_WINDOWEDGE,增强窗体的3D感观。窗体风格改用 WS_OVERLAPPEDWINDOW,创建一个带标题栏、可变大小的边框、菜单和最大化/最小化按钮的窗体。
dwExStyle=WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;
dwStyle=WS_OVERLAPPEDWINDOW;
}
// 扩展窗体风格
// 窗体风格
 
下一行代码根据创建的窗体类型调整窗口。调整的目的是使得窗口大小正好等于我们要求的分辨率。通常边框会占用窗口的一部分。使用AdjustWindowRectEx 后,我们的OpenGL场景就不会被边框盖住。实际上窗口变得更大以便绘制边框。全屏模式下,此命令无效。
AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle); // 调整窗口达到真正要求的大小
下一段代码开始创建窗口并检查窗口是否成功创建。我们将传递CreateWindowEx()所需的所有参数。如扩展风格、类名字(与您在注册窗口类时所用的名字相同)、窗口标题、窗体风格、窗体的左上角坐标(0,0 是个安全的选择)、窗体的宽和高。我们没有父窗口,也不想要菜单,这些参数被设为NULL。还传递了窗口的实例,最后一个参数被设为NULL。

注意我们在窗体风格中包括了 WS_CLIPSIBLINGS 和 WS_CLIPCHILDREN。要让OpenGL正常运行,这两个属性是必须的。他们阻止别的窗体在我们的窗体内/上绘图。
if (!(hWnd=CreateWindowEx( dwExStyle,
"OpenGL",
title,
WS_CLIPSIBLINGS |
WS_CLIPCHILDREN |
dwStyle,
0, 0,
WindowRect.right-WindowRect.left,
WindowRect.bottom-WindowRect.top,
NULL,
NULL,
hInstance,
NULL)))
// 扩展窗体风格
// 类名字
// 窗口标题
// 必须的窗体风格属性
// 必须的窗体风格属性
// 选择的窗体属性
// 窗口位置
// 计算调整好的窗口宽度
// 计算调整好的窗口高度
// 无父窗口
// 无菜单
// 实例
// 不向WM_CREATE传递任何东东
下来我们检查看窗口是否正常创建。如果成功, hWnd保存窗口的句柄。如果失败,弹出消息窗口,并退出程序。
{
KillGLWindow();                                                             
// 重置显示区
MessageBox(NULL,"Window Creation Error.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE;                                                               
// 返回 FALSE
}
下面的代码描述象素格式。我们选择了通过RGBA(红、绿、蓝、alpha通道)支持OpenGL和双缓存的格式。我们试图找到匹配我们选定的色彩深度(16位、24位、32位)的象素格式。最后设置16位Z-缓存。其余的参数要么未使用要么不重要(stencil buffer模板缓存和accumulation buffer聚集缓存除外)。
static PIXELFORMATDESCRIPTOR pfd=
{
sizeof(PIXELFORMATDESCRIPTOR),
1,
PFD_DRAW_TO_WINDOW |
PFD_SUPPORT_OPENGL |
PFD_DOUBLEBUFFER,
PFD_TYPE_RGBA,
bits,
0, 0, 0, 0, 0, 0,
0,
0,
0,
0, 0, 0, 0,
16,
0,
0,
PFD_MAIN_PLANE,
0,
0, 0, 0
};
//pfd 告诉窗口我们所希望的东东

file://上诉格式描述符的大小
// 版本号
// 格式必须支持窗口
// 格式必须支持OpenGL
// 必须支持双缓冲
// 申请 RGBA 格式
// 选定色彩深度
// 忽略的色彩位
// 无Alpha缓存
// 忽略Shift Bit
// 无聚集缓存
// 忽略聚集位
// 16位 Z-缓存 (深度缓存)
// 无模板缓存
// 无辅助缓存
// 主绘图层
// 保留
// 忽略层遮罩
 
如果前面创建窗口时没有错误发生,我们接着尝试取得OpenGL设备描述表。若无法取得DC,弹出错误消息程序退出(返回FALSE)。
if (!(hDC=GetDC(hWnd)))                                                      //取得设备描述表了么?
{
KillGLWindow();                                                             
// 重置显示区
MessageBox(NULL,"Can't Create A GL Device Context.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE;                                                               
// 返回 FALSE
}
设法为OpenGL窗口取得设备描述表后,我们尝试找到对应与此前我们选定的象素格式的象素格式。如果Windows不能找到的话,弹出错误消息,并退出程序(返回FALSE)。
if (!(PixelFormat=ChoosePixelFormat(hDC,&pfd)))                             // Windows 找到相应的象素格式了吗?
{
KillGLWindow();                                                            
// 重置显示区
MessageBox(NULL,"Can't Find A Suitable PixelFormat.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE;                                                              
// 返回 FALSE
}
Windows 找到相应的象素格式后,尝试设置象素格式。如果无法设置,弹出错误消息,并退出程序(返回FALSE)。
if(!SetPixelFormat(hDC,PixelFormat,&pfd))                                   // 能够设置象素格式么?
{
KillGLWindow();                                                    
        // 重置显示区
MessageBox(NULL,"Can't Set The PixelFormat.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE;                                                 
             // 返回 FALSE
}
正常设置象素格式后,尝试取得着色描述表。如果不能取得着色描述表的话,弹出错误消息,并退出程序(返回FALSE)。
if (!(hRC=wglCreateContext(hDC)))                                           // 能否取得着色描述表?
{
KillGLWindow();
                                                             // 重置显示区
MessageBox(NULL,"Can't Create A GL Rendering Context.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE;
                                                               // 返回 FALSE
}
如果到现在仍未出现错误的话,我们已经设法取得了设备描述表和着色描述表。接着要做的是激活着色描述表。如果无法激活,弹出错误消息,并退出程序(返回FALSE)。
if(!wglMakeCurrent(hDC,hRC))                                               // 尝试激活着色描述表
{
KillGLWindow();                                                    
        // 重置显示区
MessageBox(NULL,"Can't Activate The GL Rendering Context.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE;                                                 
             // 返回 FALSE
}
一切顺利的话,OpenGL窗口已经创建完成,接着可以显示它啦。将它设为前端窗口(给它更高的优先级),并将焦点移至此窗口。然后调用ReSizeGLScene 将屏幕的宽度和高度设置给透视OpenGL屏幕。
ShowWindow(hWnd,SW_SHOW);
SetForegroundWindow(hWnd);
SetFocus(hWnd);
ReSizeGLScene(width, height);
// 显示窗口
// 略略提高优先级
// 设置键盘的焦点至此窗口
// 设置透视 GL 屏幕
跳转至 InitGL(),这里可以设置光照、纹理、等等任何需要设置的东东。您可以在 InitGL()内部自行定义错误检查,并返回 TRUE (一切正常)或FALSE (有什么不对)。例如,如果您在InitGL()内装载纹理并出现错误,您可能希望程序停止。如果您返回 FALSE的话,下面的代码会弹出错误消息,并退出程序。
if (!InitGL())                                                              // 初始化新建的GL窗口
{
KillGLWindow();                                                            
// 重置显示区
MessageBox(NULL,"Initialization Failed.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE;                                                              
// 返回 FALSE
}
到这里可以安全的推定创建窗口已经成功了。我们向WinMain()返回TRUE,告知WinMain()没有错误,以防止程序退出。
return TRUE; // 成功
}
下面的代码处理所有的窗口消息。当我们注册好窗口类之后,程序跳转到这部分代码处理窗口消息。
LRESULT CALLBACK WndProc( HWND hWnd, 
UINT uMsg, 
WPARAM wParam, 
LPARAM lParam) 
{
// 窗口的句柄
// 窗口的消息
// 附加的消息内容
// 附加的消息内容
 
下来的代码比对uMsg的值,然后转入case处理,uMsg 中保存了我们要处理的消息名字。
switch (uMsg) 
{
// 检查Windows消息
 
如果uMsg等于WM_ACTIVE,查看窗口是否仍然处于激活状态。如果窗口已被最小化,将变量active设为FALSE。如果窗口已被激活,变量active的值为TRUE。
case WM_ACTIVATE: 
{
if (!HIWORD(wParam)) 
{
active=TRUE; 
}
else
{
active=FALSE; 
}
return 0; 
}
// 监视窗口激活消息

// 检查最小化状态

// 程序处于激活状态



// 程序不再激活

// 返回消息循环
 
如果消息是WM_SYSCOMMAND(系统命令),再次比对wParam。如果wParam 是 SC_SCREENSAVE 或 SC_MONITORPOWER的话,不是有屏幕保护要运行,就是显示器想进入节电模式。返回0可以阻止这两件事发生。
case WM_SYSCOMMAND:
{
switch (wParam) 
{
case SC_SCREENSAVE: 
case SC_MONITORPOWER: 
return 0; 
}
break; 
}
// 中断系统命令Intercept System Commands

// 检查系统调用Check System Calls

// 屏保要运行?
// 显示器要进入节电模式?
// 阻止发生

// 退出

 
如果 uMsg是WM_CLOSE,窗口将被关闭。我们发出退出消息,主循环将被中断。变量done被设为TRUE,WinMain()的主循环中止,程序关闭。
case WM_CLOSE:
{
PostQuitMessage(0); 
return 0; 
}
//收到Close消息?

// 发出退出消息

 
 
如果键盘有键按下,通过读取wParam的信息可以找出键值。我将键盘数组keys[ ]相应的数组组成员的值设为TRUE。这样以后就可以查找key[ ]来得知什么键被按下。允许同时按下多个键。
case WM_KEYDOWN: 
{
keys[wParam] = TRUE; 
return 0; 
}
// 有键按下么?

// 如果是,设为TRUE
// 返回

 
 
同样,如果键盘有键释放,通过读取wParam的信息可以找出键值。然后将键盘数组keys[ ]相应的数组组成员的值设为FALSE。这样查找key[ ]来得知什么键被按下,什么键被释放了。键盘上的每个键都可以用0-255之间的一个数来代表。举例来说,当我们按下40所代表的键时,keys[40]的值将被设为TRUE。放开的话,它就被设为FALSE。这也是key数组的原理。
case WM_KEYUP: 
{
keys[wParam] = FALSE; 
return 0; 
}
// 有键放开么?

// 如果是,设为FALSE
// 返回

 
当调整窗口时,uMsg 最后等于消息WM_SIZE。读取lParam的LOWORD 和HIWORD可以得到窗口新的宽度和高度。将他们传递给ReSizeGLScene(),OpenGL场景将调整为新的宽度和高度。
case WM_SIZE: 
{
ReSizeGLScene(LOWORD(lParam),HIWORD(lParam));  
return 0; 
}
}
// 调整OpenGL窗口大小

// LoWord=Width,HiWord=Height
// 返回


 
其余无关的消息被传递给DefWindowProc,让Windows自行处理。
//向 DefWindowProc传递所有未处理的消息。
return DefWindowProc(hWnd,uMsg,wParam,lParam);
}
下面是我们的Windows程序的入口。将会调用窗口创建例程,处理窗口消息,并监视人机交互。
int WINAPI WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine, 
int nCmdShow) 
{
//实例
// 前一个实例
// 命令行参数
// 窗口显示状态
 
 
我们设置两个变量。msg 用来检查是否有消息等待处理。done的初始值设为FALSE。这意味着我们的程序仍未完成运行。只要程序done保持FALSE,程序继续运行。一旦done的值改变为TRUE,程序退出。
MSG msg; 
BOOL done=FALSE; 
// Windowsx消息结构
// 用来退出循环的Bool 变量
这段代码完全可选。程序弹出一个消息窗口,询问用户是否希望在全屏模式下运行。如果用户单击NO按钮,fullscreen变量从缺省的TRUE改变为FALSE,程序也改在窗口模式下运行。
// 提示用户选择运行模式
if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)
{
fullscreen=FALSE;
file://窗口模式
}
接着创建OpenGL窗口。CreateGLWindow函数的参数依次为标题、宽度、高度、色彩深度,以及全屏标志。就这么简单!我很欣赏这段代码的简洁。如果未能创建成功,函数返回FALSE。程序立即退出。
// 创建OpenGL窗口
if (!CreateGLWindow("NeHe's OpenGL Framework",640,480,16,fullscreen))
{
return 0;
// 失败退出
}
 
下面是循环的开始。只要done保持FALSE,循环一直进行。
while(!done) // 保持循环直到 done=TRUE
{
 
我们要做的第一件事是检查是否有消息在等待。使用PeekMessage()可以在不锁住我们的程序的前提下对消息进行检查。许多程序使用GetMessage(),也可以很好的工作。但使用GetMessage(),程序在收到paint消息或其他别的什么窗口消息之前不会做任何事。
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
 //有消息在等待吗?
 
 
下面的代码查看是否出现退出消息。如果当前的消息是由PostQuitMessage(0)引起的WM_QUIT,done变量被设为TRUE,程序将退出。
if (msg.message==WM_QUIT)
{
done=TRUE;
}
else
{
//收到退出消息?

// 是,则done=TRUE

// 不是,处理窗口消息
 
如果不是退出消息,我们翻译消息,然后发送消息,使得WndProc() 或 Windows能够处理他们。
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
else
{
// 翻译消息
// 发送消息


// 如果没有消息
 
如果没有消息,绘制我们的OpenGL场景。代码的第一行查看窗口是否激活。如果按下ESC键,done变量被设为TRUE,程序将会退出。
// 绘制场景。监视ESC键和来自DrawGLScene()的退出消息
if (active)
{
if (keys[VK_ESCAPE])
{
done=TRUE;
}
else
{

// 程序激活的么?

// ESC 按下了么?

// ESC 发出退出信号

// 不是退出的时候,刷新屏幕
 
如果程序是激活的且ESC没有按下,我们绘制场景并交换缓存(使用双缓存可以实现无闪烁的动画)。我们实际上在另一个看不见的"屏幕"上绘图。当我们交换缓存后,我们当前的屏幕被隐藏,现在看到的是刚才看不到的屏幕。这也是我们看不到场景绘制过程的原因。场景只是即时显示。
DrawGLScene();
SwapBuffers(hDC);
}
}
// 绘制场景
// 交换缓存 (双缓存)

下面的一点代码是最近新加的(05-01-00)。允许用户按下F1键在全屏模式和窗口模式间切换。
if (keys[VK_F1])
{
keys[VK_F1]=FALSE;
KillGLWindow();
fullscreen=!fullscreen;
// 重建 OpenGL 窗口
if (!CreateGLWindow("NeHe's OpenGL Framework",640,480,16,fullscreen))
{
return 0;
}
}
}
}
//  F1键按下了么?

// 若是,使对应的Key数组中的值为 FALSE
// 销毁当前的窗口
// 切换 全屏 / 窗口 模式



// 如果窗口未能创建,程序退出



 
If the done variable is no longer FALSE, the program quits. We kill the OpenGL window properly so that everything is freed up, and we exit the program.
如果done变量不再是FALSE,程序退出。正常销毁OpenGL窗口,将所有的内存释放,退出程序。
// 关闭程序
KillGLWindow();
return (msg.wParam);
}

// 销毁窗口
// 退出程序

在这一课中,我已试着尽量详细解释一切。每一步都与设置有关,并创建了一个全屏OpenGL程序。当您按下ESC键程序就会退出,并监视窗口是否激活。我花了整整2周时间来写代码,一周时间来改正BUG并讨论编程指南,2天( 整整22小时来写HTML文件)。如果您有什么意见或建议请给我EMAIL。如果您认为有什么不对或可以改进,请告诉我。我想做最好的OpenGL教程并对您的反馈感兴趣。

03 marzo

遇到了个强人

今天突然被QQ群里一个人加为好友,问我申请科大控制科学phd的要求。我建议她申请美国,人家说听力不好不敢申请,我还以为是英语听力不好,建议她好好补一下。惊讶的是她告诉我她基本属于失聪状态,低频是中度失聪,高频是极重度失聪。这样的居然还是哈工大控制科学硕士即将毕业。除了震惊还是震惊,等于是自学完成了硕士学业,哈工大的控制科学怎么说也算是比较牛的呢。
 
仰慕的一米,突然发现自己简直就是浪费生命啊。自己已经习惯了懒惰,终于遇到了一个强人,榜样。。。。。。