这三课的基础都是第五课的那个正方体,无论是纹理映射或是光照键盘还是混合,都是对立方体进行的操作。当然,把正方体换成别的几何物体比如说一个多边形面或三棱锥,操作还是同样的。纹理映射是针对面的,光照是针对光源周围的3D空间的,混合是针对纹理的。而键盘是作为总体控制的。
一、纹理映射
简单的来说,纹理映射就是把一幅图贴到一个面上,在这里,由于技术限制,我们只能用2^n像素(如256*256像素)的bmp格式的图片,可以用画图软件或ps等改变图片的像素大小和存储格式。
首先是一个打开文件的函数,AUX_RGBImageRec *LoadBMP(char *Filename)。我们用这个函数来打开我们准备的BMP图片。接下来是int LoadGLTextures(),这个函数将我们打开的图转为纹理,并存到texture数组中。
然后将 Status 设为 TRUE,glGenTextures(3, &texture[0])创建纹理,glGenTextures函数的作用是创建一个纹理的索引(参见百度百科),接下来是三种纹理滤波方式。
//创建 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);
// 创建线性滤波纹理
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);
// 创建 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);
gluBuild2DMipmaps(GL_TEXTURE_2D, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
两个glTexParameteri告诉OpenGL在显示图像时,当它比放大得原始的纹理大 ( GL_TEXTURE_MAG_FILTER )或缩小得比原始得纹理小( GL_TEXTURE_MIN_FILTER )时OpenGL采用的滤波方式。
第一种使用Nearest 滤波,从原理上讲,这种方式没有真正进行滤波。它只占用很小的处理能力,看起来也很差。唯一的好处是这样我们的工程在很快和很慢的机器上都可以正常运行。可以注意到我们在 MIN 和 MAG 时都采用了GL_NEAREST, 你可以混合使用 GL_NEAREST 和 GL_LINEAR。纹理看起来效果会好些,但我们更关心速度,所以全采用低质量贴图。MIN_FILTER在图像绘制时小于贴图的原始尺寸时采用。MAG_FILTER在图像绘制时大于贴图的原始尺寸时采用。
第二种线性滤波,它使得纹理从很远处到离屏幕很近时都平滑显示,这也是相对常用的一种滤波方式。
第三种Mipmapping 滤波,当图像在屏幕上变得很小的时候,很多细节将会丢失。刚才还很不错的图案变得很难看。当您告诉OpenGL创建一个 mipmapped的纹理后,OpenGL将尝试创建不同尺寸的高质量纹理。当您向屏幕绘制一个 mipmapped纹理的时候,OpenGL将选择它已经创建的外观最佳的纹理(带有更多细节)来绘制,而不仅仅是缩放原先的图像(这将导致细节丢失)。在这里,有办法可以绕过OpenGL对纹理宽度和高度所加的限制——64、128、256,等等,就是 gluBuild2DMipmaps。可以使用任意的位图来创建纹理。OpenGL将自动将它缩放到正常的大小。
最后就是常规性的检查了,看纹理是否存在,纹理图像是否存在,若存在则释放内存和图像结构。
以上这些纹理的内容都在int LoadGLTextures()函数中。
二、光照
光照的实现较为简单。
首先要在InitGL()函数中增加有关光照的参数设定,这是在透视修正后进行的。在这里用到了glLightfv这个函数。这个函数有三个参数,第一个参数说明了我们要设置的是几号光源,第三个参数是存放了数据的数组,而第二个参数说明了第三个参数的用途,比如环境光、漫反射或光源位置。最后用glEnable(GL_LIGHT1)启用光源。要注意的是到目前为止我们仅仅是设定好了光源并让它处于可以打开的状态,但它并没有真正打开,对于光源而言,只有glEnable(GL_LIGHTING)才能让它亮起来。
接下来在DrawGLScene函数中出现的是一个新的内容:法线。我们都知道法线是垂直于平面的向量,因此法线的主要作用就是设定光发出的方向,是从里向外还是从外向里,是照亮哪个面的,等等这些问题。关于法线,Nehe的解释很详细。总的来说,以正方形为例,有光源时我们要增加的就是每画一个面都要先有一句类似glNormal3f(0.0f, 0.0f, 1.0f)这样设定法线的,由于正常情况下光线应该指向物体外部,所以要注意在正面法线指向观察者,而在背面,法线则背离观察者,其他面同理。这样就使得我们在旋转物体时物体总是亮的。
最后,一切参数都设定好了,我们到主函数中控制它是否发光。这里用到的键盘控制很简单,按下L键则启用光源(glEnable(GL_LIGHTING)),再次按下则禁用光源(glDisable(GL_LIGHTING))。
三、键盘控制
键盘虽然是在第7课中出现,但在前面的内容中都可以用到,目前我见到的键盘控制主要有两种,一种是通过按下某个键来实现某种效果是否出现,比如说按下L则灯亮或灭,按下F1则在全屏和窗口切换,等等;一种是通过按同一个键来实现在好几种相似情况下切换,比如说一直按下F,会在几种纹理处理方式之间切换,一直按空格会在三角形、四边形、多边形等之间切换。我把这两种切换分别叫做布尔切换和选择切换,要注意,这两种说法纯粹是个人想法,为了记起来方便,目前大概就我一个人这样说……
1、布尔切换
之所以说是布尔切换,是因为这种切换和布尔代数一样,有两个状态,事实上这个切换也确实是通过布尔代数来实现的。
首先要定义两个全局变量,比如 BOOL light 和 BOOL lp,前一个跟踪当前状态,在这里是光源是否打开,后一个是存储按键是否按下,在这里是”L”键。
接下来就跳到主程序中,在SwapBuffers(hDC) 这一句后面加键盘控制。先回顾一下SwapBuffers(hDC)这句,其作用是交换缓存。交换缓存后我们绘制的图像就显示出来,接下来就可以通过键盘来控制了。
首先,要检查L键是否在没被按下的前提下被按下(这句话好绕…),Nehe 教程上对这句的注释是“ L 键已按下并且松开了?”,个人对这样的注释不是特别理解(这样听起来好像这个if可以检测出按下后松开没…)。如果我们在L键没被按下的情况下按下了L键,那么就把lp的值设为TRUE,然后通过这句light = !light来切换光源状态,即,如果光源亮着,那么就让光源灭了,如果光源没亮,那就让它亮了。接下来我们还要检查是否存在光源,如果没有,就禁用光源(glDisable(GL_LIGHTING)),如果有,就启用光源(glEnable(GL_LIGHTING))。在这里,要说一下glEnable和glDisable这两个函数,glEnable用于启用各种功能,glDisable用以关闭各项功能。函数参数应为unsigned int 类型,参数具体取值为OpenGl所预设的常量,如下表:(这个表真的不很重要,可以跳过不看…)
类型 | 值 | 说明 |
---|---|---|
GL_ALPHA_TEST | 4864 | 根据函数glAlphaFunc的条件要求来决定图形透明的层度是否显示。具体参见glAlphaFunc |
GL_AUTO_NORMAL | 3456 | 执行后,图形能把光反射到各个方向 |
GL_BLEND | 3042 | 启用颜色混合。例如实现半透明效果 |
GL_CLIP_PLANE0 ~ GL_CLIP_PLANE5 | 12288 ~ 12283 | 根据函数glClipPlane的条件要求 启用图形切割管道。这里指六种缓存管道 |
GL_COLOR_LOGIC_OP | 3058 | 启用每一像素的色彩为位逻辑运算 |
GL_COLOR_MATERIAL | 2930 | 执行后,图形(材料)将根据光线的照耀进行反射。 反射要求由函数glColorMaterial进行设定。 |
GL_CULL_FACE | 2884 | 根据函数glCullFace要求启用隐藏图形材料的面。 |
GL_DEPTH_TEST | 2929 | 启用深度测试。 根据坐标的远近自动隐藏被遮住的图形(材料) |
GL_DITHER | 3024 | 启用抖动 |
GL_FOG | 2912 | 雾化效果 例如距离越远越模糊 |
GL_INDEX_LOGIC_OP | 3057 | 逻辑操作 |
GL_LIGHT0 ~ GL_LIGHT7 | 16384 ~ 16391 | 启用0号灯到7号灯(光源) 光源要求由函数glLight函数来完成 |
GL_LIGHTING | 2896 | 启用灯源 |
GL_LINE_SMOOTH | 2848 | 执行后,过虑线段的锯齿 |
GL_LINE_STIPPLE | 2852 | 执行后,画虚线 |
GL_LOGIC_OP | 3057 | 逻辑操作 |
GL_MAP1_COLOR_4 | 3472 | 根据函数Map1对贝赛尔曲线的设置,启用glEvalCoord1,glEvalMesh1,glEvalPoint1生成RGBA曲线 |
GL_MAP1_INDEX | 3473 | 根据函数Map1对贝赛尔曲线的设置,启用glEvalCoord1,glEvalMesh1,glEvalPoint1生成颜色索引曲线 |
GL_MAP1_NORMAL | 3474 | 根据函数Map1对贝赛尔曲线的设置,启用glEvalCoord1,glEvalMesh1,glEvalPoint1生成法线 |
GL_MAP1_TEXTURE_COORD_1 | 3475 | 根据函数Map1对贝赛尔曲线的设置,启用glEvalCoord1,glEvalMesh1,glEvalPoint1生成文理坐标 |
GL_MAP1_TEXTURE_COORD_2 | 3476 | 根据函数Map1对贝赛尔曲线的设置,启用glEvalCoord1,glEvalMesh1,glEvalPoint1生成文理坐标 |
GL_MAP1_TEXTURE_COORD_3 | 3477 | 根据函数Map1对贝赛尔曲线的设置,启用glEvalCoord1,glEvalMesh1,glEvalPoint1生成文理坐标 |
GL_MAP1_TEXTURE_COORD_4 | 3478 | 根据函数Map1对贝赛尔曲线的设置,启用glEvalCoord1,glEvalMesh1,glEvalPoint1生成文理坐标 |
GL_MAP1_VERTEX_3 | 3479 | 根据函数Map1对贝赛尔曲线的设置,启用glEvalCoord1,glEvalMesh1,glEvalPoint1在三维空间里生成曲线 |
GL_MAP1_VERTEX_4 | 3480 | 根据函数Map1对贝赛尔曲线的设置,启用glEvalCoord1,glEvalMesh1,glEvalPoint1在四维空间里生成法线 |
GL_MAP2_COLOR_4 | 3504 | 根据函数Map2对贝赛尔曲线的设置,启用glEvalCoord2,glEvalMesh2,glEvalPoint2生成RGBA曲线 |
GL_MAP2_INDEX | 3505 | 根据函数Map2对贝赛尔曲线的设置,启用glEvalCoord2,glEvalMesh2,glEvalPoint2生成颜色索引 |
GL_MAP2_NORMAL | 3506 | 根据函数Map2对贝赛尔曲线的设置,启用glEvalCoord2,glEvalMesh2,glEvalPoint2生成法线 |
GL_MAP2_TEXTURE_COORD_1 | 3507 | 根据函数Map2对贝赛尔曲线的设置,启用glEvalCoord2,glEvalMesh2,glEvalPoint2生成纹理坐标 |
GL_MAP2_TEXTURE_COORD_2 | 3508 | 根据函数Map2对贝赛尔曲线的设置,启用glEvalCoord2,glEvalMesh2,glEvalPoint2生成纹理坐标 |
GL_MAP2_TEXTURE_COORD_3 | 3509 | 根据函数Map2对贝赛尔曲线的设置,启用glEvalCoord2,glEvalMesh2,glEvalPoint2生成纹理坐标 |
GL_MAP2_TEXTURE_COORD_4 | 3510 | 根据函数Map2对贝赛尔曲线的设置,启用glEvalCoord2,glEvalMesh2,glEvalPoint2生成纹理坐标 |
GL_MAP2_VERTEX_3 | 3511 | 根据函数Map2对贝赛尔曲线的设置,启用glEvalCoord2,glEvalMesh2,glEvalPoint2在三维空间里生成曲线 |
GL_MAP2_VERTEX_4 | 3512 | 根据函数Map2对贝赛尔曲线的设置,启用glEvalCoord2,glEvalMesh2,glEvalPoint2在三维空间里生成曲线 |
GL_NORMALIZE | 2977 | 根据函数glNormal的设置条件,启用法向量 |
GL_POINT_SMOOTH | 2832 | 执行后,过虑线点的锯齿 |
GL_POLYGON_OFFSET_FILL | 32823 | 根据函数glPolygonOffset的设置,启用面的深度偏移 |
GL_POLYGON_OFFSET_LINE | 10754 | 根据函数glPolygonOffset的设置,启用线的深度偏移 |
GL_POLYGON_OFFSET_POINT | 10753 | 根据函数glPolygonOffset的设置,启用点的深度偏移 |
GL_POLYGON_SMOOTH | 2881 | 过虑图形(多边形)的锯齿 |
GL_POLYGON_STIPPLE | 2882 | 执行后,多边形为矢量画图 |
GL_SCISSOR_TEST | 3089 | 根据函数glScissor设置,启用图形剪切 |
GL_STENCIL_TEST | 2960 | 开启使用模板测试并且更新模版缓存。参见glStencilFunc和glStencilOp |
GL_TEXTURE_1D | 3552 | 启用一维纹理 |
GL_TEXTURE_2D | 3553启用二维文理 | |
GL_TEXTURE_GEN_Q | 3171 | 根据函数glTexGen,启用纹理处理 |
GL_TEXTURE_GEN_R | 3170 | 根据函数glTexGen,启用纹理处理 |
GL_TEXTURE_GEN_S | 3168 | 根据函数glTexGen,启用纹理处理 |
GL_TEXTURE_GEN_T | 3169 | 根据函数glTexGen,启用纹理处理 |
在我们启用/禁用光源之后,退出关于按下L键的if语句,然后就要判断一下L键是否放开了,如果放开了,就把light设为FALSE。
2、选择切换
选择切换,是根据我们定义的一个GLuint变量的值,或通过switch语句,或通过数组下标,或是其他能想到的方式,来选择我们执行哪一种情况。
比如说数组,在第7课中,三种处理方式的纹理分别存在texture[0]、texture[1]和texture[2]中,通过变量filter的取值,在DrawGLScene函数的绑定纹理的语句glBindTexture,用glBindTexture(GL_TEXTURE_2D, texture[filter]); 来实现切换,并在主函数中,增加语句,使得每次按下F键,filter的值都加一,当filter为2时再加一就变成0 。然后还要检查按下后放开没,如果放开就把对应键的跟踪值设为false。代码如下:
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
}
如果是switch语句,那么同样是在DrawGLScene函数中,通过switch来表示在不同情况下绘制不同的图形,在主函数中控制switch的变量在按键的情况下加一,值满则回到初始,和上面的一样。
四、混合(混色)
混合这里真的很简单,一个函数glBlendFunc()设定好就完成了。但作为数学渣表示完全看不懂这个函数……
忽略掉那些数学式子,先来看程序中的混合过程。混合主要是指图像的透明度混合,说的专业一些就是红绿蓝和 alpha 源/目标混合因子进行计算,使得图像的色调和透明度有了变化。
混合的实现只有两行:
glColor4f(1.0f,1.0f,1.0f,0.5f); // 全亮度, 50% Alpha 混合
glBlendFunc(GL_SRC_ALPHA,GL_ONE); // 基于源象素alpha通道值的半透明混合函数
在Nehe的教程中,这两行是出现在InitGL函数中,作为OpenGL的整体环境的设置。很容易理解,这两行分别设定了亮度和混合方式。可以试着把第一行中的参数进行修改,会看到不同的效果,比如都是1.0f,那么看到的图像就很亮,并且接近黑白的效果。第二行是glBlendFunc函数,glBlendFunc有两个参数,前者表示源因子,后者表示目标因子。这两个参数可以是多种值,比如这里是GL_SRC_ALPHA 和 GL_ONE,那么得到的图像就是半透明状态;如果是glBlendFunc(GL_ONE, GL_ZERO),那么得到的图像就是没有混合的样子。这个函数很重要,关于它的更详细的解释可以看这个链接: http://blog.csdn.net/aurora_mylove/article/details/1700540 。关于混合函数的数学方面的内容,可以看百度百科中的介绍。
设置好混合参数以后,就要在主函数中启用混合,这里的方法和光照的启用和关闭完全一样,不过要注意的是一旦启用混合就要关了深度测试,一关了混合就要启用深度测试。如果在混合时没关深度测试,那么正方体的某几个面会出现像阴影一样的效果,如果关了混合没开深度测试,那么正方体的几个面就会像是不完整一样。
在实现了一个正方体的混合之后,实现两个甚至更多的正方体、棱柱、棱锥等等的混合就很容易了,都是一样的内容,只是多画几个点。接下来面临的问题是如果有两个贴图,而我们需要其中一个透明另一个不透明,或者说两个贴图要采用不同的混合方式该怎么办。因为InitGL函数是针对整个场景的环境设置,因此这个问题就显得有些麻烦。
其实解决办法也很简单,就是不在InitGL函数中设置混合的方式,而是在DrawGLScene函数中,因为InitGL函数是对整个场景的设置,而DrawGLScene是对每个物体、每个面的设置。以两个正方体为例,载入两个贴图并转为纹理后,在DrawGLScene函数中,画正方体之前,根据混合的启用与否的判断,加入if语句,然后是我们之前提到的那两行,和启用混合以及关闭深度测试,else语句中再关闭混合并启用深度测试。同样的,当多个物体要有不同的混合方式时,就在每个物体前面加这样一组if-else语句。两个正方体的例子下载:http://download.csdn.net/detail/u014420201/8504201 。