{ 这一课我会教您如何使用三种不同的纹理滤波方式。 教您如何使用键盘来移动场景中的对象,还会教您在OpenGL场景中应用简单的光照。 这一课包含了很多内容,如果您对前面的课程有疑问的话,先回头复习一下。 进入后面的代码之前,很好的理解基础知识十分重要。 我们还是在第一课的代码上加以修改。 跟以前不一样的是,只要有任何大的改动,我都会写出整段代码。 首先我们还要加进SysUtils单元和Glaux单元。 } Uses SysUtils, opengl, windows, Messages, Glaux In '..\..\GLAUX\Glaux.pas'; //下面几行是增加新的变量。 //我们增加三个布尔变量。 // light 变量跟踪光照是否打开。 //变量lp和fp用来存储'L' 和'F'键是否按下的状态。 //后面我会解释这些变量的重要性。现在,先放在一边吧。 light : Boolean; // 光源的开/关 lp : Boolean; // L键按下了么? fp : Boolean; // F键按下了么? //现在设置5个变量来控制绕x轴和y轴旋转角度的步长, //以及绕x轴和y轴的旋转速度。 //另外还创建了一个z变量来控制进入屏幕深处的距离。 xrot : GLfloat; // X 旋转 yrot : GLfloat; // Y 旋转 xspeed : GLfloat; // X 旋转速度 yspeed : GLfloat; // Y 旋转速度 z : GLfloat = -5.0 f; // 深入屏幕的距离 //接着设置用来创建光源的数组。 //我们将使用两种不同的光。 //第一种称为环境光。环境光来自于四面八方。 //所有场景中的对象都处于环境光的照射中。 //第二种类型的光源叫做漫射光。 //漫射光由特定的光源产生,并在您的场景中的对象表面上产生反射。 //处于漫射光直接照射下的任何对象表面都变得很亮, //而几乎未被照射到的区域就显得要暗一些。 //这样在我们所创建的木板箱的棱边上就会产生的很不错的阴影效果。 //创建光源的过程和颜色的创建完全一致。 //前三个参数分别是RGB三色分量,最后一个是alpha通道参数。 //因此,下面的代码我们得到的是半亮(0.5f)的白色环境光。 //如果没有环境光,未被漫射光照到的地方会变得十分黑暗。 LightAmbient : Array[0..3] Of GLfloat = (0.5, 0.5, 0.5, 1.0); //环境光参数 ( 新增 ) //下一行代码我们生成最亮的漫射光。 //所有的参数值都取成最大值1.0f。 //它将照在我们木板箱的前面,看起来挺好。 LightDiffuse : Array[0..3] Of GLfloat = (1.0, 1.0, 1.0, 1.0); // 漫射光参数 ( 新增 ) //最后我们保存光源的位置。 //前三个参数和glTranslate中的一样。 //依次分别是XYZ轴上的位移。 //由于我们想要光线直接照射在木箱的正面,所以XY轴上的位移都是0.0。 //第三个值是Z轴上的位移。 //为了保证光线总在木箱的前面, //所以我们将光源的位置朝着观察者(就是您哪。)挪出屏幕。 //我们通常将屏幕也就是显示器的屏幕玻璃所处的位置称作Z轴的0.0点。 //所以Z轴上的位移最后定为2.0。 //假如您能够看见光源的话,它就浮在您显示器的前方。 //当然,如果木箱不在显示器的屏幕玻璃后面的话,您也无法看见箱子。 //『译者注:我很欣赏NeHe的耐心。 //说真的有时我都打烦了,这么简单的事他这么废话干嘛? //但如果什么都清楚,您还会翻着这样的页面看个没完么?』 //最后一个参数取为1.0f。 //这将告诉OpenGL这里指定的坐标就是光源的位置,以后的教程中我会多加解释。 LightPosition : Array[0..3] Of GLfloat = (0.0, 0.0, 2.0, 1.0); // 光源位置 ( 新增 ) //filter 变量跟踪显示时所采用的纹理类型。 //第一种纹理(texture 0) 使用gl_nearest(不光滑)滤波方式构建。 //第二种纹理 (texture 1) 使用gl_linear(线性滤波) 方式, //离屏幕越近的图像看起来就越光滑。 //第三种纹理 (texture 2) 使用 mipmapped滤波方式, //这将创建一个外观十分优秀的纹理。 //根据我们的使用类型,filter 变量的值分别等于 0, 1 或 2 。 //下面我们从第一种纹理开始。 //texture为三种不同纹理分配储存空间。 //它们分别位于在 texture[0], texture[1] 和 texture[2]中。 filter : GLuint; // 滤波类型 texture : Array[0..2] Of GLuint; // 3种纹理的储存空间 Procedure glGenTextures(n: GLsizei; Var textures: GLuint); stdcall; external opengl32; Procedure glBindTexture(target: GLenum; texture: GLuint); stdcall; external opengl32; Function gluBuild2DMipmaps(target: GLenum; components, width, height: GLint; format, atype: GLenum; data: Pointer): Integer; stdcall; external glu32 name 'gluBuild2DMipmaps'; { 现在载入一个位图,并用它创建三种不同的纹理。 这一课使用glaux辅助库来载入位图, 因此在编译时您应该确认是否包含了glaux库。 我知道Delphi和VC++都包含了glaux库,但别的语言不能保证都有。 『译者注:glaux是OpenGL辅助库,根据OpenGL的跨平台特性, 所有平台上的代码都应通用。但辅助库不是正式的OpenGL标准库, 没有出现在所有的平台上。但正好在Win32平台上可用。 呵呵,BCB当然也没问题了。』这里我只对新增的代码做注解。 如果您对某行代码有疑问的话,请查看教程六。 那一课很详细的解释了载入、创建纹理的内容。 在上一段代码后面及 glResizeWnd ()之前的位置, 我们增加了下面的代码。这和第六课中载入位图的代码几乎相同。 } Function LoadBmp(filename: pchar): PTAUX_RGBImageRec; Var BitmapFile : Thandle; // 文件句柄 Begin If Filename = '' Then // 确保文件名已提供。 result := Nil; // 如果没提供,返回 NULL BitmapFile := FileOpen(Filename, fmOpenWrite); //尝试打开文件 If BitmapFile > 0 Then // 文件存在么? Begin FileClose(BitmapFile); // 关闭句柄 result := auxDIBImageLoadA(filename); //载入位图并返回指针 End Else result := Nil; // 如果载入失败,返回NiL。 End; Function LoadTexture: boolean; // 载入位图并转换成纹理 Var Status : boolean; // Status 指示器 TextureImage : Array[0..1] Of PTAUX_RGBImageRec; // 创建纹理的存储空间 Begin Status := false; ZeroMemory(@TextureImage, sizeof(TextureImage)); // 将指针设为 NULL TextureImage[0] := LoadBMP('Walls.bmp'); If TextureImage[0] <> Nil Then Begin Status := TRUE; // 将 Status 设为 TRUE glGenTextures(1, 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); // ( 新增 ) 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]); // 使用来自位图数据生成 的典型纹理 // 生成纹理 glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0].sizeX, TextureImage[0].sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0].data); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // 线形滤波 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 线形滤波 //下面是创建纹理的新方法。 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 是位图宽度, //TextureImage[0].sizeY 是位图高度, //(====不知为什么,delphi下这个函数没有height这个参数, //但是帮助中却有,不知delphi再搞什么,郁闷ing...... //最后我在前面自己写了一个gluBuild2DMipmaps, //来载入glu32.dll中的gluBuild2DMipmaps函数=====) //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); //(新增) } End; If assigned(TextureImage[0]) Then // 纹理是否存在 If assigned(TextureImage[0].data) Then // 纹理图像是否存在 TextureImage[0].data := Nil; // 释放纹理图像占用的内存 TextureImage[0] := Nil; // 释放图像结构 result := Status; // 返回 Status End; //接着应该载入纹理并初始化OpenGL设置了。 //GLInit函数的第一行使用上面的代码载入纹理。 //创建纹理之后,我们调用glEnable(GL_TEXTURE_2D)启用2D纹理映射。 //阴影模式设为平滑阴影( smooth shading )。 //背景色设为黑色,我们启用深度测试,然后我们启用优化透视计算。 Procedure glInit(); // 此处开始对OpenGL进行所有设置 Begin If (Not LoadTexture) Then // 调用纹理载入子例程 exit; // 如果未能载入,退出 glEnable(GL_TEXTURE_2D); // 启用纹理映射 glShadeModel(GL_SMOOTH); // 启用阴影平滑 glClearColor(0.0, 0.0, 0.0, 0.0); // 黑色背景 glClearDepth(1.0); // 设置深度缓存 glEnable(GL_DEPTH_TEST); // 启用深度测试 glDepthFunc(GL_LESS); // 所作深度测试的类型 glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //高度优化的透视投影计算 //现在开始设置光源。下面下面一行设置环境光的发光量, //光源light1开始发光。 //这一课的开始处我们我们将环境光的发光量存放在LightAmbient数组中。 //现在我们就使用此数组(半亮度环境光)。 glLightfv(GL_LIGHT1, GL_AMBIENT, @LightAmbient[0]); // 设置环境光 //接下来我们设置漫射光的发光量。它存放在LightDiffuse数组中(全亮度白光)。 glLightfv(GL_LIGHT1, GL_DIFFUSE, @LightDiffuse[0]); // 设置漫射光 //然后设置光源的位置。 //位置存放在 LightPosition 数组中 //(正好位于木箱前面的中心,X-0.0,Y-0.0,Z方向移向观察者2个单位<位于屏幕外面>)。 glLightfv(GL_LIGHT1, GL_POSITION, @LightPosition); // 光源位置 //最后,我们启用一号光源。我们还没有启用GL_LIGHTING, //所以您看不见任何光线。 //记住:只对光源进行设置、定位、甚至启用,光源都不会工作。 //除非我们启用GL_LIGHTING。 glEnable(GL_LIGHT1); // 启用一号光源 End; //下一段代码绘制贴图立方体。我只对新增的代码进行注解。 //如果您对没有注解的代码有疑问,回头看看第六课。 Procedure glDraw(); // 从这里开始进行所有的绘制 Begin glClear(GL_COLOR_BUFFER_BIT Or GL_DEPTH_BUFFER_BIT); // 清除屏幕和深度缓存 glLoadIdentity(); // 重置当前的模型观察矩阵 //下三行代码放置并旋转贴图立方体。 //glTranslatef(0.0,0.0,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.0, 0.0, z); // 移入/移出屏幕 z 个单位 glRotatef(xrot, 1.0, 0.0, 0.0); // 绕X轴旋转 glRotatef(yrot, 0.0, 1.0, 0.0); // 绕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.0, 0.0, 1.0); // 法线指向观察者 glTexCoord2f(0.0, 0.0); glVertex3f(-1.0, -1.0, 1.0); // 纹理和四边形的左下 glTexCoord2f(1.0, 0.0); glVertex3f(1.0, -1.0, 1.0); // 纹理和四边形的右下 glTexCoord2f(1.0, 1.0); glVertex3f(1.0, 1.0, 1.0); // 纹理和四边形的右上 glTexCoord2f(0.0, 1.0); glVertex3f(-1.0, 1.0, 1.0); // 纹理和四边形的左上 // 后面 glNormal3f(0.0, 0.0, -1.0); // 法线背向观察者 glTexCoord2f(1.0, 0.0); glVertex3f(-1.0, -1.0, -1.0); // 纹理和四边形的右下 glTexCoord2f(1.0, 1.0); glVertex3f(-1.0, 1.0, -1.0); // 纹理和四边形的右上 glTexCoord2f(0.0, 1.0); glVertex3f(1.0, 1.0, -1.0); // 纹理和四边形的左上 glTexCoord2f(0.0, 0.0); glVertex3f(1.0, -1.0, -1.0); // 纹理和四边形的左下 // 顶面 glNormal3f(0.0, 1.0, 0.0); // 法线向上 glTexCoord2f(0.0, 1.0); glVertex3f(-1.0, 1.0, -1.0); // 纹理和四边形的左上 glTexCoord2f(0.0, 0.0); glVertex3f(-1.0, 1.0, 1.0); // 纹理和四边形的左下 glTexCoord2f(1.0, 0.0); glVertex3f(1.0, 1.0, 1.0); // 纹理和四边形的右下 glTexCoord2f(1.0, 1.0); glVertex3f(1.0, 1.0, -1.0); // 纹理和四边形的右上 // 底面 glNormal3f(0.0, -1.0, 0.0); // 法线朝下 glTexCoord2f(1.0, 1.0); glVertex3f(-1.0, -1.0, -1.0); // 纹理和四边形的右上 glTexCoord2f(0.0, 1.0); glVertex3f(1.0, -1.0, -1.0); // 纹理和四边形的左上 glTexCoord2f(0.0, 0.0); glVertex3f(1.0, -1.0, 1.0); // 纹理和四边形的左下 glTexCoord2f(1.0, 0.0); glVertex3f(-1.0, -1.0, 1.0); // 纹理和四边形的右下 // 右面 glNormal3f(1.0, 0.0, 0.0); // 法线朝右 glTexCoord2f(1.0, 0.0); glVertex3f(1.0, -1.0, -1.0); // 纹理和四边形的右下 glTexCoord2f(1.0, 1.0); glVertex3f(1.0, 1.0, -1.0); // 纹理和四边形的右上 glTexCoord2f(0.0, 1.0); glVertex3f(1.0, 1.0, 1.0); // 纹理和四边形的左上 glTexCoord2f(0.0, 0.0); glVertex3f(1.0, -1.0, 1.0); // 纹理和四边形的左下 // 左面 glNormal3f(-1.0, 0.0, 0.0); // 法线朝左 glTexCoord2f(0.0, 0.0); glVertex3f(-1.0, -1.0, -1.0); // 纹理和四边形的左下 glTexCoord2f(1.0, 0.0); glVertex3f(-1.0, -1.0, 1.0); // 纹理和四边形的右下 glTexCoord2f(1.0, 1.0); glVertex3f(-1.0, 1.0, 1.0); // 纹理和四边形的右上 glTexCoord2f(0.0, 1.0); glVertex3f(-1.0, 1.0, -1.0); // 纹理和四边形的左上 glEnd(); xrot := xrot + xspeed; // xrot 增加 xspeed 单位 yrot := Yrot + yspeed; // yrot 增加 yspeed 单位 End; //现在转入WinMain()主函数。 //我们将在这里增加开关光源、旋转木箱、切换过滤方式以及将木箱移近移远的控制代码。 //在接近WinMain()函数结束的地方你会看到SwapBuffers(hDC)这行代码。 //然后就在这一行后面添加如下的代码。 //代码将检查L键是否按下过。 //如果L键已按下,但lp的值不是false的话,意味着L键还没有松开,这时什么都不会发生。 SwapBuffers(h_DC); // 交换缓存 (双缓存) If (keys[ord('L')] And Not lp) Then Begin //如果lp的值是false的话, //意味着L键还没按下,或者已经松开了,接着lp将被设为TRUE。 //同时检查这两个条件的原因是为了防止L键被按住后, //这段代码被反复执行,并导致窗体不停闪烁。 //lp设为true之后,计算机就知道L键按过了, //我们则据此可以切换光源的开/关:布尔变量light控制光源的开关。 lp := true; // lp 设为 TRUE light := Not light; // 切换光源的 TRUE/FALSE If Not light Then // 如果没有光源 glDisable(GL_LIGHTING) //禁用光源 Else // Otherwis glEnable(GL_LIGHTING); //启用光源 End; If Not keys[ord('L')] Then //L键松开了么? lp := FALSE; // 若是,则将lp设为FALSE //然后对"F"键作相似的检查。 //如果有按下"F"键并且"F"键没有处于按着的状态或者它就从没有按下过, //将变量fp设为true。这意味着这个键正被按着呢。 //接着将filter变量加一。如果filter变量大于2 //(因为这里我们的使用的数组是texture[3],大于2的纹理不存在), //我们重置filter变量为0。 If (keys[ord('F')] And Not fp) Then // F键按下了么? Begin fp := TRUE; // fp 设为 TRUE inc(filter); // filter的值加一 If filter > 2 Then // 大于2了么? filter := 0; // 若是重置为0 End; If Not keys[ord('F')] Then //F键放开了么? fp := FALSE; // 若是fp设为FALSE //这四行检查是否按下了PageUp键。若是的话,减少z变量的值。这样DrawGLScene函数中包含的glTranslatef(0.0f,0.0f,z)调用将使木箱离观察者更远一点。 If keys[VK_PRIOR] Then //PageUp按下了? z := z - 0.02; // 若按下,将木箱移向屏幕内部。 //接着四行检查PageDown键是否按下,若是的话,增加z变量的值。这样DrawGLScene函数中包含的glTranslatef(0.0f,0.0f,z)调用将使木箱向着观察者移近一点。 If keys[VK_NEXT] Then // PageDown按下了么? z := z + 0.02; //若按下的话,将木箱移向观察者。 //现在检查方向键。按下左右方向键xspeed相应减少或增加。 //按下上下方向键yspeed相应减少或增加。 //记住在以后的教程中如果xspeed、yspeed的值增加的话,立方体就转的更快。 //如果一直按着某个方向键,立方体会在那个方向上转的越快。 If keys[VK_UP] Then // Up方向键按下了么? xspeed := xspeed - 0.01; //若是,减少xspeed If keys[VK_DOWN] Then //Down方向键按下了么? xspeed := xspeed + 0.01; //若是,增加xspeed If keys[VK_RIGHT] Then //Right方向键按下了么? yspeed := yspeed + 0.01; //若是,增加yspeed If keys[VK_LEFT] Then //Left方向键按下了么? yspeed := yspeed - 0.01; //若是, 减少yspeed If (keys[VK_ESCAPE]) Then // 如果按下了ESC键 finished := True 运行一下看看效果