NeHe+OpenGL教程 第七课 纹理过滤器、光照和键盘控制

在这节课中,我将教你如何使用三种不同的纹理过滤器。我将教你如何使用键盘来移动物体,如何在OpenGL场景中使用简单的光照。这一课包含了很多内容,如果你对前面的课程有疑问的话,先回头复习一下。在你学习下面的知识之前,对基础知识的熟练掌握很重要。

我们再次地修改第一节课中的代码。通常,如果某一部分代码做了重要的改动,我会把这部分代码全部重写。我们首先给程序添加一些新的变量。

#include <windows.h>  // Windows头文件
#include    <stdio.h>  // 标准输入/输出头文件 ( ADD )
#include <gl\gl.h>    // OpenGL32库头文件
#include <gl\glu.h>   // GLu32库头文件
#include <gl\glaux.h> // GLaux库头文件
HGLRC      hRC=NULL;         // 永久的渲染上下文( Rendering Context)
HDC                hDC=NULL;         // 私有的GDI设备上下文( GDI Device Context)
HWND              hWnd=NULL;        // 获得我们窗口的句柄
HINSTANCE   hInstance;        // 获得应用程序的实例
bool   keys[256];              // 用于键盘行为的数组
bool   active=TRUE;            // 窗口活动标记,默认设置为TRUE
bool   fullscreen=TRUE;        // 全屏标记,默认设置为全屏
下面的这几行代码都是新加的。我们添加三个boolean型变量。BOOL型变量的值为TRUE或FALSE。我们定义一个light变量来检测光照是打开状态还是关闭状态。变量lp和fp用来检测‘L’键和‘F’键是否按下。我会在后面解释这些变量的用处。现在,只需要知道这些变量有重要的用途。

BOOL    light;    // 光照打开/关闭
BOOL    lp;    // L键是否按下
BOOL    fp;      // F键是否按下

接下来,我们定义五个变量,xrot用来控制在x轴上的转动角度,yrot用来控制y轴上的转动角度,xspeed用来控制条板箱在x轴上的转动速度,yspeed用来控制条板箱在y轴上的转动速度。z变量用来控制条板箱在屏幕里的深度。

GLfloat xrot;        // x轴上的转动角度
GLfloat yrot;       // y轴上的转动角度
GLfloat xspeed;        // x轴上的转动速度
GLfloat yspeed;        // y轴上的转动速度
GLfloat z=-5.0f;      //在屏幕里的深度

然后,我们设置用来产生光线的数组。我们将使用两种不同类型的光。第一种类型的光叫做环绕光。环绕光是没有特定方向的光线。场景中的任何物体都会被环绕光照亮。第二种类型的光叫做发散光。发散光是由你设置的光源产生的,它能被场景中的物体表面反射。直接被光线照射的物体表面会很亮,而光线少的那一面会很暗。这会使条板箱的侧面产生一个不错的阴影效果。光线的生成方式和颜色的生成方式是一样的。如果第一个参数是1.0f,接下来的两个参数都是0.0f,将产生一个明亮的红光。如果第三个参数是1.0f,而前两个参数是0.0f,将产生一个明亮的蓝光。最后一个参数是alpha值,我们暂且把它设置为1.0f。

所以,下面的代码生成了一个半强度的白光。因为所有的参数都是0.5f,我们将产生一个灰白的光。(Because all the numbers are 0.5f, we will end up with a light that's halfway between off (black) and full brightness (white). )红、绿和蓝以相同的值混合,将形成一个从黑到白的色调。如果没有环绕光,没有被散射光找到的部分将会非常暗。

GLfloat LightAmbient[]= { 0.5f, 0.5f, 0.5f, 1.0f };    // 环绕光( NEW )

下面的代码,我们将生成一个非常明亮的,强度最大的白光。所有的参数都为1.0f。意味着这是我们能够产生的最亮的光。散射光源放置在条板箱的正前方。

GLfloat LightDiffuse[]= { 1.0f, 1.0f, 1.0f, 1.0f };              // 散射光 ( NEW )

最后,我们保存光源的位置。前三个参数和glTranslate函数的前三个参数的用处是相同的。第一个参数用来在x屏幕左右移动,第二个参数用来在y轴上上下移动,第三个参数用来在z轴上前后移动。因为我们想让我们的光源直接照射在条板箱的正面,我们不需要左右移动,所以第一个参数为0.0f,我们也不想上下移动,所以第二个参数也为0.0f。至于第三个参数,因为我们想让光源总是在条板箱的正前方。所以我们把光源移出屏幕。假设照相机的镜头在z平面的0.0f处。那么我们把光源的位置放在z平面的2.0f处。如果你可以实际地看到光源,你会发现它就浮在照相机镜头的正前方。这么做之后,光线唯一能照到条板箱后面的方法是,把条板箱也移到照相机镜头的前面。当然,如果条板想已经不在照相机镜头的后面了,我们就观察不到条板箱了,所以,光源在哪里并不重要。能感觉到吗?(这里可能不太好理解,我稍微解释以下。现在照相机镜头在z平面的位置为0.0f,光源在z平面的位置为2.0f,条板箱在z屏幕的位置为负数(还没确定,但肯定是负数)。因为2.0f > 0.0f,所以光源在照相机镜头的前面;因为0.0f > 负数,所以照相机镜头在条板箱的前面。总之,谁大谁在前面,谁小谁在后面。之后物体在照相机镜头的后面,才可以看见物体。)

没有更简单的方法来解释这三个参数了。你应该知道-2.0f比-5.0f和-100.0f在屏幕中的位置离你更近。一旦你到了0.0f,物体将大到充满整个屏幕。一旦z值大于0.0f,你将不会再看到物体了,因为它已经远离屏幕了。我说的远离屏幕的意思是,物体依然存在,只是你再也看不见它了。

最后一个参数为1.0f。它通知OpenGL我们指定的位置是光源的位置。我们将在以后的课程中做解释。

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

下面第一行代码的变量用来监视使用了哪一种纹理。第一种纹理(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];          // 存储三种纹理
 
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);    // 声明WndProc函数

现在,我们加载一张bitmap图像,然后用它生成三种不同的纹理。教程中使用glaux函数库来加载bitmap图像,所以在编译代码之前,确认你已经引入了glaux函数库。我知道Delphi和 Visual C++ 都有glaux函数库。我不确定其它的语言有没有。我将解释新的代码的作用,如果你发现哪些代码我没有注释,而你又想知道它的用途,回头看第六课的教程。它很详细地解释了如何加载bitmap图像,并用它来生成纹理。

紧接着上面的代码,在ReSizeGLScene()函数之前,我们要加入下面的代码。下面的代码和第六课教程中用来加载bitmap图像的代码完全一样。没有做任何修改。如果你对下面的代码还有什么疑问,回头看第六课教程,它解释的很详细。

AUX_RGBImageRec *LoadBMP(char *Filename)  // 加载一幅Bitmap图像
{
    FILE *File=NULL;   // 文件句柄

if (!Filename) //检查是否已经有了一个要加载文件的文件名
{
    return NULL;  // 如果没有,返回 NULL
}

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

if (File)  // 文件是否存在
{
    fclose(File); // 如果存在,关闭文件句柄
    return auxDIBImageLoad(Filename);  // 加载Bitmap图像,返回一个指针
}

   return NULL;   // 如果加载失败,返回 NULL
}

下面这部分代码是用来加载bitmap(调用上面的代码),然后把它转换成3种纹理映射。Status变量是用来判断纹理是否加载和创建成功。

int LoadGLTextures()      // 加载bitmap,然后把它转换成纹理
{
    int Status=FALSE;  // Status 标识位
 
    AUX_RGBImageRec *TextureImage[1];   // 为纹理创建存储空间
 
    memset(TextureImage,0,sizeof(void *)*1);  // 设置指针为NULL
现在我们加载bitmap,然后把它转换成纹理。TextureImage[0]=LoadBMP("Data/Crate.bmp") 这行代码将会跳转到LoadBMP()这部分代码。在Data文件夹下的Crate.bmp文件将会加载进来。如果一切顺利,这个图像数据将会保存在TextureImage[0]中,Status变量设置为TRUE,然后我们创建纹理。

// 加载bitmap,检查是否出错,如果bitmap没有找到,退出
if (TextureImage[0]=LoadBMP("Data/Crate.bmp"))
{
    Status=TRUE;       // Status 设置为 TRUE

现在我们已经把图像数据加载到了TextureImage[0]中,我们将使用这个数据创建三个纹理。下面的代码通知OpenGL我们将生成三个纹理,这些纹理将被存储在texture[0],texture[1]和texture[2]中。

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

在第六课中,我们使用了线性过滤纹理映射。这需要大量的处理器资源,但是看起来效果很好。这节课我们使用的第一个纹理类型是GL_NEAREST。基本上这种类型的纹理没有过滤效果。它需要很少的处理器资源,但是效果很差。如果你曾经玩过一些纹理贴图看起来像块状的游戏的话,它可能使用的就是这种类型的纹理。这种类型纹理的唯一好处就是,可以在速度很慢的机器上运行流畅。

你会注意到我们在MIN 和 MAG上都使用的是GL_NEAREST 类型的纹理。你也可以把GL_NEAREST 和 GL_LINEAR混合使用,这样纹理效果看起来会好一点,但是我们专注于速度,所以我们都使用低质量类型的纹理。MIN_FILTER 过滤器是当绘制的图像比原始纹理小的时候使用的。而MAG_FILTER 过滤器的情况正好相反。

// 创建 Nearest 过滤器纹理
glBindTexture(GL_TEXTURE_2D, texture[0]);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST); // ( NEW )
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST); // ( NEW )
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);

下一种类型的纹理和我们在第六课中使用的纹理类型是一样的,Linear 过滤器纹理。唯一的区别是我们把它存储在texture[1]中而不是texture[0]中,因为它是第二个纹理。如果我们想上面那样把它存储在texture[0]中,它将会覆盖GL_NEAREST 纹理(第一个纹理)。

// 创建Linear 过滤器纹理
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等2的幂次方的限制的方法。gluBuild2DMipmaps 就是这个方法。至少我发现,创建mipmapped 纹理时,你可以使用任何你想用的bitmap图像(任何的宽和高)。OpenGL将会自动调整它到合适的宽和高。

因为这个第三个纹理,所以我们把它存储在texture[2]中。所以现在,我们有一个没有任何过滤效果的纹理——texture[0],有一个使用线性过滤效果的纹理——texture[1],和一个使用mipmapped 效果的纹理——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); // ( NEW )

下面这行代码创建MipMapped 纹理。我们使用三种颜色(红,绿,蓝)创建一个2D纹理。TextureImage[0]->sizeX是bitmap的宽,TextureImage[0]->sizeY是bitmap的高,GL_RGB 是说我们以红,绿,蓝的顺序使用颜色,GL_UNSIGNED_BYTE 是说纹理数据的类型是byte类型,TextureImage[0]->data是指向我们用来创建纹理的bitmap数据。

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

然后我们释放所有我们用来存储bitmap数据的存储空间。我们检查bitmap数据是否存储在TextureImage[0]中。如果是,我们再检查数据是否存储在里面。如果数据已经存储在里面,我们释放它。然后我们释放这个图像结构体,确保使用过的存储空间全部释放。

if (TextureImage[0])           //如果纹理存在

{
    if (TextureImage[0]->data)     // 如果纹理图像存在 
    {
        free(TextureImage[0]->data);             // 释放纹理图像内存 
    }
 
    free(TextureImage[0]);                      // 释放图像结构体
}

最后,我们返回Status变量。如果一切顺利,Status变量的值为TRUE。如果出现错误,Status变量的值为FALSE。

    return Status;             // 返回Status 变量
}

现在我们加载纹理,然后初始化OpenGL设置。InitGL 函数的第一行代码使用上面的代码加载纹理。纹理创建之后,我们使用glEnable(GL_TEXTURE_2D)函数启用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);   // 完美透视计算

接下来我们设置光照。下面的代码我们设置环绕光强度,light1 光源启用。在这节课的开始部分我们把环绕光的强度存储在LightAmbient变量中。我们将使用存储在这个数组中的值。(半强度环绕光)

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

接下来,我们设置散射光的强度,light1 光源启用。我们存储散射光的强度在LightDiffuse变量中。我们将使用存储在这个数组中的值。(饱和强度的白光)

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

现在我们设置光源的位置。我们把光源的位置存储在LightPosition变量中。我们将使用存储在这个数组中的值。(正好在正面的中心,在x轴的位置为0.0f,在y轴的位置为 0.0f ,在z平面的位置为朝向观察者2个单位处 {移出屏幕} )

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

最后我们启用光源light1 。我们还没有启用GL_LIGHTING ,所以现在你还不能看到任何的光照效果。光源已经设置好了,光源的位置也已经设置了,它也已经启用了,但是直到我们启用GL_LIGHTING ,否者光源不会起作用。

    glEnable(GL_LIGHT1);                            // 启用光源 light1
    return TRUE;                                // 初始化完成

}

下面这部分代码我们将绘制纹理映射的正方体。一些新代码会有注释。如果你不太清楚没有注释的代码的用途,看一下第六课的教程。

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轴上旋转正方体xrot度

glRotatef(yrot,0.0f,1.0f,0.0f);      // 在y轴上旋转正方体yrot度

下面的这行代码和我们在第六课中使用的很相似,只是我们使用texture[filter]来代替texture[0]。每一次我们点击'F'键,filter的值都会增加。如果这个值大于2,它会被重新设置为0。程序开始时filter的默认值为0。这和调用glBindTexture(GL_TEXTURE_2D, texture[0])的效果是一样的。通过使用filter变量我们可以选择我们创建的三个纹理中的任意一个。

glBindTexture(GL_TEXTURE_2D, texture[filter]);      // 根据filter变量选择一个纹理
 
glBegin(GL_QUADS);        // 开始绘制正方形

glNormal3f 函数是课程里新添加的。法线是垂直于多边形平面的指向外面的直线。当你使用光照时,你需要明确的指明法线。法线通知OpenGL多边形的正面朝向哪里。如果你不指明法线,会有一些意想不到的事情发生。不应该被照亮的一面被照亮了,不应该被照亮的一侧被照亮了,等等。法线的方向应该是垂直于多边形向外的。(由背面到正面)

观察正方体的正面你会发现,法线的z值是正值。这说明法线是指向观察者的。正好是我们想要的方向。在正方体的背面,法线是远离观察者的,指向屏幕的里面。也是我们想要的方向。如果正方体沿x轴或者y轴旋转180度,正面将会面向屏幕里面,而背面将会朝向观察者。无论哪一面朝向观察者,那个面的法线都是指向观察者的。因为光源更接近观察者,所以无论何时法线指向观察者它也就指向了光源。当它朝向光源时,那一面就会被照亮。法线越接近光源,那一面就会越亮。在正方体的内部,是漆黑的。因为法线的方向是向外而不是向内,所以在正方体的内部没有光线,实际上也应如此。

glBegin(GL_QUADS);
    // 正面

  glNormal3f( 0.0f, 0.0f, 1.0f);     // 法线指向观察者 
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f,  1.0f);  // 左下角
    glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f,  1.0f);  // 右下角
    glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f,  1.0f,  1.0f);  // 右上角
    glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f,  1.0f,  1.0f);  // 左上角
    // 背面

  glNormal3f( 0.0f, 0.0f,-1.0f);     //法线远离观察者

    glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);  // 右下角
    glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f,  1.0f, -1.0f);  // 右上角
    glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f,  1.0f, -1.0f);  // 左上角
    glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);  // 左下角
    // 上面

  glNormal3f( 0.0f, 1.0f, 0.0f);    // 法线指向上面 
    glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f,  1.0f, -1.0f);  // 左上角
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f,  1.0f,  1.0f);  // 左下角
    glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f,  1.0f,  1.0f);  // 右下角
    glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f,  1.0f, -1.0f);  // 右上角
    // 底面

  glNormal3f( 0.0f,-1.0f, 0.0f);   // 法线指向下面 
    glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f);  // 右上角
    glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f);  // 左上角
    glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f,  1.0f);  // 左下角
    glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f,  1.0f);  // 右下角
    // 右面

  glNormal3f( 1.0f, 0.0f, 0.0f);    // 法线指向右边

    glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);  // 右下角
    glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f,  1.0f, -1.0f);  // 右上角
    glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f,  1.0f,  1.0f);  // 左上角
    glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f,  1.0f);  // 左下角
    // 左面

  glNormal3f(-1.0f, 0.0f, 0.0f);   // 法线指向左边 
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);  // 左下角
    glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f,  1.0f);  // 右下角
    glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f,  1.0f,  1.0f);  // 右上角
    glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f,  1.0f, -1.0f);  // 左上角
glEnd();
下面的两行代码通过存储在xspeed和yspeed变量中的值来增加xrot 和 yrot 变量的值。在xspeed和yspeed变量中的值来越大,xrot 和 yrot 变量的值增加的越快。xrot 和 yrot 变量的值增加的越快,正方体在那个轴上旋转的越快。

    xrot+=xspeed;          // xrot的值加上xspeed的值 
    yrot+=yspeed;          // yrot的值加上yspeed的值

    return TRUE;                        

}

然后我们到WinMain()函数中。增加代码来控制光照的开/关,旋转正方体,改变纹理,把正方体移入/移出屏幕。在WinMain()函数接近底部的地方,你会看到SwapBuffers(hDC)函数调用。紧接着这行代码,加入下面的代码。

代码检查键盘上的'L'键是否被按下。第一行代码检查'L'键是否被按下。如果'L'键被按下了,但是lp不等于false,意味着'L'键已经被按下一次或者'L'键被按住了,这时不会做任何处理。

SwapBuffers(hDC);               // 交换缓冲区 (双缓冲)
if (keys['L'] && !lp)               // L键被按住?
{

如果lp为false,意味着'L'键没有被按下,或者已经松开,lp设置为true。它强制你要想让这段代码再次运行必须松开'L'键。如果你不检查'L'键是否被按住,光源将会一遍一遍的打开和关闭,因为程序假设当运行到这段代码时每一次你都点击了'L'键。

一旦lp被设置为true,通知计算机'L'键已经被按住了,我们打开或关闭光源。light变量只有true或false两种值。所以如果我们通过light = !light;我们是说让light取反。翻译过来就是如果light等于true,设置light不等于true(false),如果light等于false,设置light不等于false(true)。所以如果light等于true,就设置它为false,如果light等于false,就设置它为true。

lp=TRUE;                // lp 设置为TRUE
light=!light;               // 设置 Light 为 TRUE/FALSE

然后我们检查最后的light值。第一行是说:如果light等于false。所以整个if语句的意思是,如果light等于false,关闭光源。它将关闭所有光源。else分支是说,如果light等于true,打开光源。

    if (!light)             // Light等于false
    {
        glDisable(GL_LIGHTING);     // 关闭光源 
    }
    else                    // 否则 
    {
        glEnable(GL_LIGHTING);      // 打开光源 
    }
}

下面的代码检查我们是否松开了'L'键。如果我们松开了'L'键,设置lp等于false,说明'L'键没有按住。如果我们不检查'L'键是否松开了,我们可以打开一次光源,但是因为计算机一直认为'L'键是被按下的,所以我们不能再关闭它。

if (!keys['L'])                 // 'L'键是否松开?
{
    lp=FALSE;               // 如果松开了, lp 设置为FALSE
}

然后,我们用同样的方法处理'F'键。如果'F'键被按下,并且它没有被按住或者它重来没有被按过,这时设置fp为true,说明现在'F'键被按住了。它将增加filter变量的值。如果filter变量大于2,我们把它重置为0。

if (keys['F'] && !fp)               //  F键是否被按下?
{
    fp=TRUE;                // fp 设置为TRUE
    filter+=1;              // filter 变量加1

    if (filter>2)                // 如果大于2?
    {
        filter=0;           // 重置为0 
    }
}
if (!keys['F'])                 // F键是否松开?
{
    fp=FALSE;               // 如果松开,fp 设置为FALSE
}

下面的四行代码检查我们是否按下了'PAGE UP键。如果我们按下了,将会减小z的值,因为在DrawGLScene 函数中调用了glTranslatef(0.0f,0.0f,z),所以正方体会移入屏幕。

if (keys[VK_PRIOR])              //  Page Up键是否按下?
{
    z-=0.02f;               // 如果按下, 正方体移入屏幕

}

下面的四行代码检查我们是否按下了'PAGE DOWN'键。如果我们按下了,将会增加z的值,因为在DrawGLScene 函数中调用了glTranslatef(0.0f,0.0f,z),所以正方体会朝向观察者移动。

if (keys[VK_NEXT])              //  Page Down 键是否按下?
{
    z+=0.02f;               // 如果按下, 朝向观察者移动正方体

}

现在,我们检查方向键。通过点击左箭头/右箭头,xspeed将会增加/减小。通过点击上箭头/下箭头,yspeed将会增加/减少。记住我在课程开始时说过的,是否xspeed或yspeed越大,正方体旋转的越快。你按住一个方向键时间越长,正方体在那个方向上旋转的越快。

if (keys[VK_UP])                //  Up Arrow 键是否被按下?
{
    xspeed-=0.01f;              // 减小xspeed值
}
if (keys[VK_DOWN])              // Down Arrow键是否被按下?
{
    xspeed+=0.01f;              // 增加xspeed值
}
if (keys[VK_RIGHT])             //  Right Arrow 键是否被按下?
{
    yspeed+=0.01f;              // 增加yspeed 值
}
if (keys[VK_LEFT])              // Left Arrow 键是否被按下?
{
    yspeed-=0.01f;              // 减小yspeed值
}

和前面的教程一样,确保窗口的标题是正确的。

if(keys[VK_F1])  //如果F1被按下

{

             keys[VK_F1]=FALSE;  //设置为FALSE

             KillGLWindow();  //销毁当前的窗口

             fullscreen=!fullscreen;  //切换全屏/窗口模式

             // Recreate Our OpenGL Window 

            if(!CreateGLWindow("NeHe's Textures,lighting & Keyboard Tutorial",640,480,16,fullscreen))

                  {

                       return0;  //如果窗口创建失败,程序退出

                    }

                 }


                        }

        }
    }
  
    // Shutdown
    KillGLWindow();     // 杀掉窗口
    return(msg.wParam);   // 退出程序
}

在这节课的最后,你应该能够创建一个高质量的,更逼真的,有纹理映射的正方体,并可以和它交互。你应该理解这节课中三种不同纹理各自的好处。通过点击指定的按键,你应该可以跟屏幕上的物体交互,最后,你应该知道如何在场景中使用光照,来使场景更加逼真。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值