OpenGL学习笔记——纹理贴图

来源:互联网 发布:windows安装光盘修复 编辑:程序博客网 时间:2024/04/20 12:57

OpenGL学习笔记——纹理贴图

时间 2015-06-09 20:17:20 To be a better coder
原文  http://codercdy.com/openglxue-xi-bi-ji-wen-li-tie-tu-2/
主题 OpenGL

简单地说,纹理就是矩形的数据数组。例如,颜色数据、亮度数据、颜色和alpha数据。纹理数组中的单个值常常称为纹理单元(texel)。纹理 贴图之所以复杂,是因为矩形的纹理可以映射到非矩形的区域,并且必须以合理的方式实现。由于纹理是由离散的纹理单元构成,所以必须执行过滤操作,把纹理单 元映射到片断上。如果纹理单元的边界位于片断的边界上,OpenGL就会对所有相关的纹理单元求加权平均值。

每个纹理对象都表示一个单独的纹理,有些OpenGL实现可能支持一种特别的纹理对象工作集,位于工作集内部的纹理对象比位于工作集外部的纹理单元具有更高的性能。这些高性能的纹理对象称为常驻纹理对象,它们可能使用专用的硬件或软件加速工具。

概述和示例

  1. 创建纹理对象,并为它指定一个纹理;
  2. 确定纹理如何应用到每个像素上;
  3. 启用纹理贴图功能;
  4. 绘制场景、提供纹理和几何坐标。
void init(void){        glClearColor(0.0, 0.0, 0.0, 0.0);        glShadeModel(GL_FLAT);        glEnable(GL_DEPTH_TEST);        makeCheckImage();        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);#ifdef GL_VERSION_1_1        glGenTextures(1, &texName);        glBindTexture(GL_TEXTURE_2D, texName);#endif        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);#ifdef GL_VERSION_1_1        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, checkImageWidth, checkImageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, checkImage);#else        glTexImage2D(GL_TEXTURE_2D, 0, 4, checkImageWidth, checkImageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, checkImage);#endif}void display(void){        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);        glEnable(GL_TEXTURE_2D);        glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);#ifdef GL_VERSION_1_1        glBindTexture(GL_TEXTURE_2D, texName);#endif        glBegin(GL_QUADS);        glTexCoord2f(0.0, 0.0); glVertex3f(-2.0, -1.0, 0.0);        glTexCoord2f(0.0, 1.0); glVertex3f(-2.0, 1.0, 0.0);        glTexCoord2f(1.0, 1.0); glVertex3f(0.0, 1.0, 0.0);        glTexCoord2f(1.0, 0.0); glVertex3f(0.0, -1.0, 0.0);        glTexCoord2f(0.0, 0.0); glVertex3f(1.0, -1.0, 0.0);        glTexCoord2f(0.0, 1.0); glVertex3f(1.0, 1.0, 0.0);        glTexCoord2f(1.0, 1.0); glVertex3f(2.41421, 1.0, -1.41421);        glTexCoord2f(1.0, 0.0); glVertex3f(2.41421, -1.0, -1.41421);        glEnd();        glFlush();        glDisable(GL_TEXTURE_2D);}

所有纹理贴图初始化是在init()函数中进行的。glGenTextures()和glBindTexture()函数分别用于命名纹理图像以 及创建纹理对象。这个单幅的全分辨率纹理图是由glTexImage2D()函数指定的,这个函数的参数指定了这幅纹理图像的大小、位置、类型以及其他属 性。glTexParameter*()函数指定了这个纹理的包装形式,并且指定了当纹理图像中的纹理单元与屏幕上德像素并不完全匹配时,纹理的颜色应该 如何进行过滤。

在display()函数内部,glEnable()函数启用了纹理功能,glTexEnv*()函数把绘图模式设置为GL_REPLACE。这样,经过纹理贴图的多边形在绘制时就使用来自纹理图像的颜色,而不再考虑它的原来颜色。

指定纹理

glTexImage2D()函数用于定义二维纹理。

纹理图像的内部格式可能会影响纹理操作的性能。例如,有些OpenGL实现在执行纹理操作时使用GL_RGBA格式比使用GL_RGB格式更快,因为前者的颜色成分正好在处理器内存中满足4字节的对齐。

纹理图像的内部格式也影响纹理图像所消耗的内存。

GL_DEPTH_COMPONENT纹理存储了深度值,与颜色相比,它更常用来渲染阴影。类似地,GL_DEPTH_STENCIL纹理存储了相同纹理中的深度和模板值。

当内部格式采用*_SRGB_*,纹理会拥有sRGB颜色空间,sRGB颜色空间近似地与gamma校正线性RGB颜色空间相同,对于sRGB纹理,纹理中的alpha值不应该是gamma校正的。

对于后缀为"F"、"I"和"UI"的内部格式,纹理单元的格式分别存储在指定位数的浮点值、带符号整数值和无符号整数值中。对于这些格式,这些值并不是映射到范围0-1,而是允许它们的完整数字精度。

如果OpenGL实现支持图像处理子集,并且已经启用了一些图像处理子集功能,纹理图像就会受到这些功能的影响。例如,如果启用了二维卷积过滤器,那么在纹理图像上也会进行卷积过滤处理。

纹理图像的宽度和高度上的纹理单元的数量必须是2的整数次方(OpenGL2.0没有这个要求)。如果源图像不满足这个要求,可以使用工具库函数gluScaleImage()更改纹理图像的大小。

帧缓冲区本身也可以作为纹理数据使用哪个,glCopyTexImage2D()函数从帧缓冲区读取一块矩形像素,并且把它作为一个新纹理的纹理单元。

  • 纹理代理

    对于使用 纹理贴图的OpenGL程序员来说,纹理的大小是非常重要的。纹理资源一般都是有限的,而纹理格式的限制也因不同的OpenGL实现而异。OpenGL提 供了一种特殊的纹理目标,称为纹理代理,可用于判断当前的OpenGL实现在某种特定的纹理大小下是否支持某种特定的纹理格式。

    使 用GL_MAX*TEXTURE_SIZE所进行的查询并没有考虑纹理的内部格式以及其他因素的效果。一幅以GL_RGBA16内部格式存储纹理单元的纹 理图像可能用64位表示每个纹理单元,因此它的最大大小可能要比使用GL_LUMINACNE4内部格式的纹理图像要小16倍。那些要求边框或 mipmap的纹理会进一步减少可用内存的数量。

    纹理代理是一种特殊的占位符,它允许更精确地查询OpenGL是否可以容纳某种内部格式的纹理图像。

  • 替换纹理图像的全部或一部分

    在使用glTexSubImage2D()函数时,纹理图像的宽度和高度并不一定需要是2的整数次方。但是,必须把视频图像加载到一幅更大的初始图像中,后者的宽度和高度必须是2的整数次方个纹理单元,并调整子图像的纹理坐标。

    另外,帧缓冲区也可以作为纹理数据的来源,这次它所扮演的角色是纹理子图像。

    纹 理矩形:OpenGL3.1增加了通过纹理单元位置而不是规范化的纹理坐标来定位的纹理。这些纹理通过GL_TEXTURE_RECTANGLE的 target来指定,若想要在渲染的时候直接把纹理单元映射为像素,这么做就很有用。纹理矩形有自己的限制,例如不能进行基于mipmap的过滤,也不能 是压缩的。

  • 一维纹理

    一维纹理就像是一个高度为1的二维图像,并且它的底部和顶部没有边框。所有的二维纹理和子纹理定义函数都存在对应的一维版本。

  • 三维纹理

    三维纹理最常见的应用是医学和地球科学领域的渲染,是一大类应用范畴的一部分,称为体渲染。由于三维纹理可能非常大,它所消耗的纹理资源可能相当多。即使是一个相对较为粗糙的三维纹理所使用的纹理内存也可能比一个二维纹理使用的纹理内存多16或32倍。

  • 纹理数组

    如果纹理 对象需要在OpenGL服务器中更新,为每次绘制调用而调用glBindTexture()可能会影响到应用程序的性能(这可能是纹理存储资源的短处)。 纹理数组允许把一维或二维纹理的一个集合组织起来,所有的纹理都具有相同的大小,都位于更高维度的一个纹理中(例如,二维纹理的数组是三维纹理的一部 分)。

    此外,纹理数组允许在通过索引访问的纹理中进行合适的mipmap过滤。

  • 压缩纹理图像

    纹理图像在内部可以用一种压缩格式存储,以减少它所使用的内存数量。纹理图像可以在加载时进行压缩,也可以直接以一种压缩格式加载。

    • 在加载时压缩纹理图像

      为了让OpenGL在加载纹理图像时对它进行压缩,可以把internalFormat参数设置为其中一种GL_COMPRESSED_*枚举值。当纹理单元被任何活动的像素存储模式或像素传输模式处理之后,图像就会自动进行压缩。

    • 加载经过压缩的纹理图像

      OpenGL并没有指定纹理压缩应该使用的内部格式。每种OpenGL实现都可以指定一组OpenGL扩展,实现一种特定的纹理压缩格式。对于直接加载的压缩纹理,知道它们的存储格式并在OpenGL实现中验证这种纹理格式的有效性是非常重要的。

      为了加载一个以压缩格式存储的纹理,可以使用glCompressedTexImage*()函数。

      另外,就像未压缩的纹理一样,经过压缩的纹理也可以替换一个已经加载的纹理的全部或一部分。

  • 使用纹理边框

  • mipmap:多重细节层

    在 动态场景中,当一个纹理对象迅速地远离观察点而去时,纹理图像必须随被投影的图像一起缩小。为了实现这个效果,OpenGL必须对纹理图像进行过滤,适当 地对它进行缩小,使它在映射到物体的表面时不会产生令人不快的人工视觉效果,如闪烁或抖动等。例如,当渲染一面砖墙时,如果它离观察者很近,可以使用较大 的纹理对象,但是当这面砖墙与观察者的距离迅速加大时,在它缩小为屏幕上的单个像素之前,在经过一些过渡点时,经过过滤的纹理图像可能会出现突然的变化。

    为 了避免这种人工痕迹,可以指定一系列预先过滤的分辨率递减的纹理图像,称为mipmap。当OpenGL使用mipmap时,它会根据被贴图的物体的大小 自动确定应该使用哪个纹理。通过这个方法,纹理图像的细节层就能够适应绘制到屏幕上的图像。当物体的图像变小时,纹理图像也随之变小。mipmap要求一 些额外的计算,并需要一些纹理存储区域。但是,如果不使用mipmap,当纹理映射到更小的物体上时,当物体移动时,就会产生闪烁或抖动现象。

    为 了使用mipmap,必须提供全系列的大小为2的整数次方的纹理图像,其范围从最大值直到1x1纹理单元。例如,如果最高分辨率的纹理图像是64x16纹 理单元,还必须提供32x8、16x4、8x2、4x1、2x1和1x1的纹理图像。较小的纹理图像通常是进行了过滤的版本,是对最大的纹理图像进行适当 均缩之后的结果。较小纹理图像的每个纹理单元是更高一级分辨率的纹理图像的4个纹理单元的平均值。在实际使用种,不相关的纹理图像会使得mipmap层之 间的过渡变得极为醒目。

    为了指定这些纹理,可以依次调用glTexImage2D()函 数生成每种分辨率的纹理图像,每次调用时使用不同的level、width、height和image参数。level参数以0为起点,这个参数用于标识 整个系列的所有纹理图像。另外,为了使mipmap纹理生效,需要选择一种适当的过滤模式。

GLubyte mipmapImage32[32][32][4];GLubyte mipmapImage16[16][16][4];GLubyte mipmapImage8[8][8][4];GLubyte mipmapImage4[4][4][4];GLubyte mipmapImage2[2][2][2];GLubyte mipmapImage1[1][1][4];static GLuint texName;void makeImages(void){        int i, j;        for (i = 0; i < 32; i++) {                for (j = 0; j < 32; j++) {                        mipmapImage32[i][j][0] = 255;                        mipmapImage32[i][j][1] = 255;                        mipmapImage32[i][j][2] = 0;                        mipmapImage32[i][j][3] = 255;                }        }        for (i = 0; i < 16; i++) {                for (j = 0; j < 16; j++) {                        mipmapImage16[i][j][0] = 255;                        mipmapImage16[i][j][1] = 0;                        mipmapImage16[i][j][2] = 255;                        mipmapImage16[i][j][3] = 255;                }        }        for (i = 0; i < 8; i++) {                for (j = 0; j < 8; j++) {                        mipmapImage8[i][j][0] = 255;                        mipmapImage8[i][j][1] = 0;                        mipmapImage8[i][j][2] = 0;                        mipmapImage8[i][j][3] = 255;                }        }        for (i = 0; i < 4; i++) {                for (j = 0; j < 4; j++) {                        mipmapImage4[i][j][0] = 0;                        mipmapImage4[i][j][1] = 255;                        mipmapImage4[i][j][2] = 0;                        mipmapImage4[i][j][3] = 255;                }        }        for (i = 0; i < 2; i++) {                for (j = 0; j < 2; j++) {                        mipmapImage2[i][j][0] = 0;                        mipmapImage2[i][j][1] = 0;                        mipmapImage2[i][j][2] = 255;                        mipmapImage2[i][j][3] = 255;                }        }        mipmapImage1[0][0][0] = 255;        mipmapImage1[0][0][1] = 255;        mipmapImage1[0][0][2] = 255;        mipmapImage1[0][0][3] = 255;}void init(void){        glEnable(GL_DEPTH_TEST);        glShadeModel(GL_FLAT);        glTranslatef(0.0, 0.0, -3.6);        makeImages();        glPixelStoref(GL_UNPACK_ALIGNMENT, 1);        glGenTextures(1, &texName);        glBindTexture(GL_TEXTURE_2D, texName);        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,                                        GL_NEAREST_MIPMAP_NEAREST);        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 32, 32, 0,                                 GL_RGBA, GL_UNSIGNED_BYTE, mipmapImage32);        glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, 16, 16, 0,                                 GL_RGBA, GL_UNSIGNED_BYTE, mipmapImage16);        glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA, 8, 8, 0,                                 GL_RGBA, GL_UNSIGNED_BYTE, mipmapImage8);        glTexImage2D(GL_TEXTURE_2D, 3, GL_RGBA, 4, 4, 0,                                 GL_RGBA, GL_UNSIGNED_BYTE, mipmapImage4);        glTexImage2D(GL_TEXTURE_2D, 4, GL_RGBA, 2, 2, 0,                                 GL_RGBA, GL_UNSIGNED_BYTE, mipmapImage2);        glTexImage2D(GL_TEXTURE_2D, 5, GL_RGBA, 1, 1, 0,                                 GL_RGBA, GL_UNSIGNED_BYTE, mipmapImage1);        glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);        glEnable(GL_TEXTURE_2D);}void display(void){        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);        glBindTexture(GL_TEXTURE_2D, texName);        glBegin(GL_QUADS);        glTexCoord2f(0.0, 0.0); glVertex3f(-2.0, -1.0, 0.0);        glTexCoord2f(0.0, 8.0); glVertex3f(-2.0, 1.0, 0.0);        glTexCoord2f(8.0, 8.0); glVertex3f(2000.0, 1.0, -6000.0);        glTexCoord2f(8.0, 0.0); glVertex3f(2000.0, -1.0, -6000.0);        glEnd();        glFlush();}
  1. 在实际应用程序中构建Mipmap

    样例使用不同颜色来说明mipmap的处理过程,而在现实应用中,需要精心定义mipmap纹理图像,使它们之间的过渡尽量显得平滑。因此,较低分辨率的纹理图像通常是原先未过滤的高分辨率纹理图像的过滤版本。

    使 用OpenGL功能来创建mipmap有几种方法。在OpenGL的最新版本中,我们使用glGenerateMipmap(),它将为绑定到一个特定的 纹理目标的当前纹理图像构建mipmap栈。如果还没有用到OpenGL3.0及其以后的实现,仍然可以让OpenGL产生mipmap。 glTexParameter*()把GL_GENERATE_MIPMAP设置为GL_TRUE,然后对于具有BASE_LEVEL的一个mipmap 的纹理单元(内部或边框)的任何改变,都将会自动引起BASE_LEVEL+1到MAX_LEVEL的所有mipmap层级的所有纹理被重新计算和替换。 而所有其他mipmap层级的纹理,包括BASE_LEVEL层级的,都将保持不变。

    假 如已经创建了第0层的mipmap,可以使用gluBuild1DMipmaps()、gluBuild2DMipmaps()或 gluBuild3DMipmaps()函数创建和定义一系列大小递减的mipmap,直到1x1纹理单元。如果源纹理的大小并不是2的整数次方,可以使 用gluBuild*DMipmaps()函数把纹理图像缩放到最邻近的2的整数次方。

    同样,如果纹理太大,可以使用gluBuild*DMipmaps()函数把纹理图像缩小为适当的大小。

    随着对细节层控制的增加,可能只需要创建gluBuild*DMipmaps()函数所创建的mipmap的一个子集。为了计算和加载mipmap层的一个子集,可以调用gluBuild*DMipmapLevels()函数。

  2. 计算Mipmap层

    那个mipmap层将作为一个特定多边形的纹理取决于纹理图像的大小和被贴图多边形的大小之间的缩放因子Ρ。

    如果λ小于等于0,纹理就小于多边形,因此需要使用放大过滤器;如果λ大于0,就需要使用缩小过滤器。如果所选择的缩小过滤器使用了mipmap,那么λ就表示mipmap层。

  3. Mipmap层的细节控制

    在默认情况下,必须为各个维的每种分辨率都提供一个mipmap,在有些实际使用中,我们可能想避免使用非常小的mipmap来表示数据。例如,我们可能想使用一种称为嵌拼得技巧,就是用几幅更小的图像组合成一幅纹理图像。

    如果必须提供非常小的纹理,组成低分辨率的嵌拼mipmap的不同字符可能会粘在一起。因此,我们可能想限制最低分辨率。

    mipmap存在的另一个问题是“跳跃”,也就是当被贴图的多边形变大或变小时所引起的不同分辨率的mipmap层突然切换。

    为 了控制mipmap层,可以向glTexParameter*()传递GL_TEXTURE_BASE_LEVEL、 GL_TEXTURE_MAX_LEVEL、GL_TEXTURE_MIN_LOD和GL_TEXTURE_MAX_LOD常量。前两个常量控制哪些 mipmap层将被使用,并因此确定需要指定哪些mipmap层。另外两个常量用于控制前面提到的缩放因子λ的活动范围。

    这 些纹理参数可以解决前面描述的一些问题。有效地使用BASE_LEVEL和MAX_LEVEL可以减少应用程序需要指定的mipmap层的数量,并因此提 高纹理资源的使用效率。选择性地使用MAX_LOD可以保持嵌拼纹理的有效性,MIN_LOD可以减少使用高分辨率纹理时产生的跳跃效果。

    BASE_LEVEL和MAX_LEVEL用于设置mipmap层的边框。BASE_LEVEL是所使用的最高分辨率的mipmap层,默认值是0。但是可以在以后更改BASE_LEVEL的值,因此可以随时添加更高分辨率的纹理。

过滤

纹理图像时正方形或长方形的,但是当他们映射到多边形的表面并转换为屏幕坐标之后,纹理图像中的单个纹理单元很少与最终屏幕图像中的单个像素形成 一一对应关系。根据物体使用的变换以及它所应用的纹理图像,屏幕上的一个像素可以对应很广的范围,可能是一个纹理单元的一小部分(放大),也可能是多个纹 理单元(缩小)。无论是哪种情况,我们很难知道具体使用的是哪些纹理值以及它们是如何匀和或插值的。为此,OpenGL允许指定几种过滤选项,确定这些计 算的细节。这些选项提供了速度和图像质量之间的权衡。另外,可以分别为放大和缩小指定不同的过滤方法。

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
参数值GL_TEXTURE_MAG_FILTERGL_NEAREST或GL_LINEARGL_TEXTURE_MIN_FILTERGL_NEAREST、GL_LINEAR、GL_NEAREST_MIPMAP_NEAREST、 GL_NEAREST_MIPMAP_LINEAR、GL_LINEAR、GL_LINEAR_MIPMAP_NEAREST或 GL_LINEAR_MIPMAP_LINEAR

如果选择了GL_NEAREST,那么最靠近像素中心的那个纹理单元将用于放大或缩小,这可能导致锯齿状的人工痕迹(有时候还比较严重)。如果选 择GL_LINEAR,那么OpenGL就会对靠近像素中心的一块2x2纹理单元取加权平均值,用于放大或缩小。当纹理坐标靠近纹理图像的边缘时,最邻近 的2x2纹理单元可能包含了纹理图像之外的内容。在这种情况下,OpenGL使用的纹理单元值取决于当前生效的环绕模式以及纹理是否具有边框。 GL_NEAREST所需要的计算量要少于GL_LINEAR,因此它的执行速度要更快一些,到那时GL_LINEAR能够提供更加平滑的效果。

GL_NEAREST通常称为点采样,GL_LINEAR通常称为双线性采样。

纹理对象

纹理对象用于存储纹理数据,以便随时使用它们。我们可以控制多个纹理,并可以返回之前加载到纹理资源的纹理。使用纹理对象通常是应用纹理的最快方 法,它能够大幅度提高应用程序的性能,因为绑定一个原有的纹理对象要比使用glTexImage*D()函数重新加载一幅纹理图像要快的多。

另外,有些OpenGL实现支持一种容量有限的高性能纹理工作集。可以使用纹理对象,把最常用的纹理加载到这种容量有限的区域中

  1. 生成纹理名称;

  2. 初次把纹理对象绑定到纹理数据上,包括图像数组和纹理属性;

  3. 如果OpenGL实现支持高性能纹理工作集,可以检查一下是否有足够空间容纳所有的纹理对象。如果空间不足,可以为各个纹理对象建立优先级,把最常用的纹理对象保存在这个工作集中;

  4. 绑定和重新绑定纹理对象,使它们的数据当前可以用于渲染纹理模型

    #define checkImageWidth      64#define checkImageHeight     64static GLubyte checkImage[checkImageHeight][checkImageWidth][4];static GLubyte otherImage[checkImageHeight][checkImageWidth][4];static GLuint texName[2];void makeCheckImage(void){        int i, j, c;        for (i = 0; i < checkImageHeight; i++) {                for (j = 0; j < checkImageWidth; j++) {                        c = (((i&0x8)==0)^((j&0x8)==0))*255;                        checkImage[i][j][0] = (GLubyte)c;                        checkImage[i][j][1] = (GLubyte)c;                        checkImage[i][j][2] = (GLubyte)c;                        checkImage[i][j][3] = (GLubyte)255;                        c = (((i&0x10)==0)^((j&0x10)==0))*255;                        otherImage[i][j][0] = (GLubyte)c;                        otherImage[i][j][1] = (GLubyte)0;                        otherImage[i][j][2] = (GLubyte)0;                        otherImage[i][j][3] = (GLubyte)255;                }        }}void init(void){        glClearColor(0.0, 0.0, 0.0, 0.0);        glShadeModel(GL_FLAT);        glEnable(GL_DEPTH_TEST);        makeCheckImage();        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);        glGenTextures(2, texName);        glBindTexture(GL_TEXTURE_2D, texName[0]);        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, checkImageWidth, checkImageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, checkImage);        glBindTexture(GL_TEXTURE_2D, texName[1]);        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, checkImageWidth, checkImageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, otherImage);        glEnable(GL_TEXTURE_2D);}void display(void){        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);        glBindTexture(GL_TEXTURE_2D, texName[0]);        glBegin(GL_QUADS);        glTexCoord2f(0.0, 0.0); glVertex3f(-2.0, -1.0, 0.0);        glTexCoord2f(0.0, 1.0); glVertex3f(-2.0, 1.0, 0.0);        glTexCoord2f(1.0, 1.0); glVertex3f(0.0, 1.0, 0.0);        glTexCoord2f(1.0, 0.0); glVertex3f(0.0, -1.0, 0.0);        glEnd();        glBindTexture(GL_TEXTURE_2D, texName[1]);        glBegin(GL_QUADS);        glTexCoord2f(0.0, 0.0); glVertex3f(1.0, -1.0, 0.0);        glTexCoord2f(0.0, 1.0); glVertex3f(1.0, 1.0, 0.0);        glTexCoord2f(1.0, 1.0); glVertex3f(2.41421, 1.0, -1.41421);        glTexCoord2f(1.0, 0.0); glVertex3f(2.41421, -1.0, -1.41421);        glEnd();        glFlush();}
    • 命名纹理对象
    • 创建和使用纹理对象

      glBindTexture() 可以完成3个不同的任务。当textureName是一个非零的无符号整数,并且应用程序第一次在这个函数中使用这个值时,这个函数会创建一个新的纹理对 象,并把这个名称分配给它。当textureName是一个以前已经创建的纹理对象时,这个纹理对象就成为活动纹理对象。如果textureName为 0,OpenGL就停止使用纹理对象,并返回到无名称的默认纹理。

    • 清除纹理对象

      如果纹理资源有限,删除纹理是释放纹理资源的有效方法之一。

    • 常驻纹理工作集

      有些OpenGL实现支持高性能的纹理工作集,称为常驻纹理。一般情况下,这些OpenGL实现具有专用的硬件来执行纹理操作,并提供有限的硬件缓存来存储纹理图像。在这种情况下,应该尽可能地使用纹理对象,因为可以把许多纹理加载到这个工作集中,并对它们加以控制。

      如 果应用程序所需要的所有纹理的大小超出了缓存的大小,有些纹理就无法放在这个工作集中。纹理资源的动态性非常强,纹理资源状态可能会在任何时候发生变化。 有些OpenGL实现在第一次使用纹理时会把它们保存在缓存里。在绘制纹理之前检查它的常驻状态是非常必要的。如果OpenGL实现没有建立高性能纹理工 作集,那么纹理对象总被认为是常驻的。

      • 纹理常驻策略
    • 可能向纹理对象分配优先级来控制纹理成为常驻纹理的机会。如果几个纹理对象具有相同的优先级,OpenGL实现一般会采用最近最少使用策略(LRU)。

      如果可以创建一个纹理工作集,并尽可能地实现最佳的纹理性能,就必须考虑自 己使用的OpenGL实现和应用程序的具体情况。例如,在视觉模拟或视频游戏中,在任何情况下都必须坚持性能优先的原则。在这种情况下,绝对不能访问非常 驻纹理,我们需要在初始化阶段加载所有的纹理,并使它们成为常驻纹理。如果并不拥有足够的纹理内存,可能需要减少所使用的纹理图像的大小、分辨率以及 mipmap层数量。如果有几个大小相同并且生命周期较短的纹理,可以使用glTexSubImage*()函数在现有的纹理对象中加载不同的图像。这个 技巧较之删除纹理并从头到尾创建新纹理要快速很多。

纹理函数

我们可以使用纹理图像对物体表面的颜色进行调整,而不是直接贴图。也可以把纹理图像的颜色与物体表面的颜色进行组合。可以通过向glTexEnv*()函数提供适当的参数来选择纹理函数。

纹理函数以及基本内部格式的组合决定了纹理的每个成分是如何应用的。纹理函数是在纹理中被选择的纹理成分以及片断的颜色值上进行操作的(注意,纹 理成分的选择是在应用了像素传输函数后进行的)。在使用glTexImage*D()函数指定纹理图像时,这个函数的第三个参数就是为每个纹理单元所选择 的内部格式。

一共6种基本的内部格式:GL_ALPHA、GL_LUMINANCE、GL_LUMINANCE_ALPHA、GL_INTENSITY、 GL_RGB和GL_RGBA。可以使用其他内部格式(例如GL_LUMINANCE6_ALPHA2或GL_R3_G3_B2)为纹理成分指定分辨率, 这些格式都是与6种基本格式之一相匹配。纹理计算最终是在RGBA模式下进行的,但是有些内部格式并不属于RGB模式。

“替换”纹理函数在不进行纹理贴图的情况下简单地取片断的颜色,然后将其丢弃,并用纹理的颜色取而代之。如果希望在物体上应用不透明的纹理,就可以使用替换纹理函数。

“贴花”纹理函数与替换纹理函数类似,但是它只适用于RGB或RGBA内部格式,并且处理alpha值的方式是不同的。在RGBA内部格式中,片 断颜色与纹理颜色的混合比例是由纹理的alpha值决定的,片断的alpha值并不会产生影响。贴花纹理函数可以用于应用alpha混合纹理,例如机翼上 的徽章。

对于“调整”纹理函数而言,片断的颜色将根据纹理图像的内容进行调整。如果基本内部格式是GL_LUMINANCE、 GL_LUMINANCE_ALPHA、GL_INTENSITY,颜色值就与相同的值相乘,因此纹理图像对片断颜色的调整结果就是片断的原来颜色(如果 亮度或强度值为1)和黑色(如果亮度或强度值为0)之间的颜色。对于GL_RGB和GL_RGBA内部格式,源颜色的每种成分与纹理中的对于值相乘。如果 不存在alpha值,它就与片断的alpha值相乘。调整纹理非常适用于光照,因为带光照的多边形的颜色可以用于衰减纹理的颜色。

”添加“纹理函数简单地把纹理颜色与片断颜色相加。如果纹理存在alpha值,它就与片断的alpha值相乘,但GL_INTENSITY格式除 外,此时是纹理强度值与片断的alpha值相加。除非精心选择了纹理颜色和片断颜色,否则使用添加纹理函数很容易导致过渡饱和的颜色或者导致颜色值被截 取。

”混合“纹理函数是唯一使用GL_TEXTURE_ENV_COLOR所指定颜色的函数。亮度、强度或颜色值的使用方式有点像把一个alpha值与片断的颜色值进行混合。

分配纹理坐标

当我们绘制进行了纹理贴图的场景时,必须为每个顶点提供物体坐标和纹理坐标。经过变换之后,物体坐标决定了应该在屏幕上的哪个地点渲染每个特定的 顶点。纹理坐标决定了纹理图像中的哪个纹理单元将分配给这个顶点。就像着色多边形和直线中两个顶点之间的插值匀和一样,顶点之间的纹理坐标也会进行插值匀 和(记住,纹理是矩形的数据数组)。

纹理坐标通常用s、t、r和q坐标来表示,以便与物体坐标(x, y, z和w)和求值器坐标(u, v)进行区分。

  • 计算正确的纹理坐标

    二维纹理是正方形或矩形的图像,一般映射到多边形模型上,而把纹理图像映射到由多边形模拟的曲面需要相当程度的艺术想象力。

  • 重复和截取纹理

    可以分配0-1范围之外的纹理坐标,并且在纹理图像中对它们进行截取或重复。可以使用”镜像“重复,也就是表面上相邻的平铺纹理图像呈镜面反转状。

    纹理单元用于纹理,在靠近纹理坐标边缘的地方,无论是边框还是纹理内部的纹理单元都可能根据一个2x2的数组进行采样。

    如果使用了截取,可以避免表面的剩余部分受到纹理的影响。为此,可以在纹理的边缘上使用alpha值0。贴花纹理函数在计算中直接使用纹理的alpha值。我们可能还需要用适当的源和目标混合因子来启用混合功能。

  • 纹理坐标自动生成

    可以使用纹理贴图生成模型的轮廓线,或者模拟具有光泽的模型对任意环境的反射,为了自动生成纹理坐标,可以使用glTexGen()函数。

    不 同的纹理坐标生成方法具有不同的用途。当纹理图像与移动的物体保持固定时,在物体坐标中指定参考平面是最合适的。因此,可以使用 GL_OBJECT_LINEAR模式把木纹图像映射到桌子的表面。为了产生移动物体的动态轮廓线,在视觉坐标(使用GL_EYE_LINEAR模式)中 指定参考平面是最为合适的。

    • 创建轮廓线

      当指定了GL_TEXTURE_GEN_MODE和GL_OBJECT_LINEAR时,纹理生成函数就是顶点的物体坐标(x0, y0, z0, w0)的线性组合生成的坐标 = p1x0 + p2y0 + p3z0 + p4w0

      可 以通过向glEnable()函数传递GL_TEXTURE_GEN_S来启用s坐标的纹理坐标生成。为了生成其他坐标,可以使用 GL_TEXTURE_GEN_T、GL_TEXTURE_GEN_R或GL_TEXTURE_GEN_Q参数。在样例中,我们只是用了一个纹理坐标来生 成轮廓线,但是,如果需要,也可以独立地生成s、t和r纹理坐标,表示这个顶点到2个或3个不同平面的距离。

    • 球体纹理

      环境 纹理的目标就是渲染具有完美反射能力的物体,它的表面颜色就是反射到人跟周围环境的颜色。为了实现环境纹理贴图,我们需要做的就是创建一幅适当的纹理图 像,并让OpenGL生成纹理坐标。环境纹理是一种近似模拟,它基于这样的假设:和光亮的物体表面相比,环境中其他物体都相距甚远。也就是说,这些物体都 可以看成是一个很大的房间内所放置的小物体。进行了这样的假设之后,为了确定光亮表面上某个点的颜色,我们需要取一条从观察点到这个点的光线,然后把这条 光线从表面反射出来。这条反射光线的方向完全决定了这个点需要绘制的颜色。在一幅平面纹理图像中对各个方向的颜色进行编码相当于把一个擦得铮亮的完美球体 放在环境中央,然后在极远处用长焦镜头对它进行拍照。就是镜头具有无限长的焦距,并且相机位于无限远处。因此,需要编码的区域就是覆盖整个纹理图像的一个 圆形区域,它与纹理图像的顶、底、左、右边缘相切。这个圆形区域之外的纹理值不会对结果产生影响,因为它们不会在环境纹理中使用。

      创建了环境纹理图像之后,需要调用OpenGL的环境纹理贴图算法。这个算法在球体表面上找到一个与被渲染的物体上的点具有相同正切面的点,并且把球体上这个点的颜色绘制成那个物体上对应点的颜色。

      为了自动生成纹理坐标,对环境贴图提供支持,可以在程序中使用如下代码:

      glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);glEnable(GL_TEXTURE_GEN_S);glEnable(GL_TEXTURE_GEN_T);

      GL_SPHERE_MAP常量为环境纹理生成适当的纹理坐标。如上述代码所示,我们需要指定s和t方向的纹理坐标。但是,我们并不需要为纹理坐标生成函数指定任何参数。

    • 立方图纹理

      立 方图纹理是一种特殊的纹理技术,它用6幅二维纹理图像构成一个以原点为中心的纹理立方体。对于每个片断,纹理坐标(s, t, r)被当作方向向量看待,每个纹理单元都表示从原点所看到的纹理立方体上的图像。立方图纹理非常适用于实现环境、反射和光照效果。立方图纹理还可以把纹理 环绕到球体物体上,使纹理单元均匀地分布于它的各个面上。

      可以调用glTexImage2D()函数6次,分别使用target函数表示立方体的各个面(+X, -X, +Y, -Y, +Z, -Z),从而创建一个立方图纹理。顾名思义,每个立方图纹理必须具有相同的维度,使立方体的每个面都由相同的数量的纹理单元组成。

      为了创建立方图纹理,可以在场景的原点上放置一架照相机,然后把相机依次对准各个轴的正方向和负方向,拍摄6幅视野为90度的“快照”。这些“快照”把3D空间划分为6个在原点相交的平截头体。

      立 方图纹理的功能与其他许多纹理操作是正交的,因此立方图纹理可以使用标准的纹理特性,如纹理边框、mipmap、复制图像、子图像以及多重纹理等。可以使 用一种特殊的纹理代理表示立方图纹理,因为立方图纹理所占用的内存常常是普通2D纹理的6倍。应该把立方图纹理视为一个整体,为它指定纹理参数并创建纹理 对象,而不是6个立方体表面分别指定纹理参数以及创建纹理对象。

      为了确定某个特定片断所 使用的纹理(以及纹理单元),当前纹理坐标(s, r, t)首先根据s、t和r的最大绝对值(主轴)以及它的符号(方向),在6个纹理中选择其一。剩余的两个坐标除以坐标的最大值,得到新的坐标(s', t'),以查找立方图纹理中那个被选择的纹理的对应纹理单元。

      虽然可以显示地计算和指定 纹理坐标,但这样做不仅繁琐,而且毫无必要。通常,可以调用glTexGen*()函数自动生成立方图纹理的坐标,这个函数可以使用两种特殊的纹理生成模 式:GL_REFLECTION_MAP(适用于环境纹理贴图)或GL_SPHERE_MAP(适用于渲染具有无限远处光源(或远处的局部光源)以及散射 反射的场景)。

多重纹理

在进行标准的纹理处理时,一次是把一幅图像应用到1个多边形上。多重纹理多重纹理允许应用几个纹理,在纹理操作管线中把它们逐个应用到同一个多边 形上。多重纹理存在一系列的纹理单位,每个纹理单位执行各自的纹理操作,并把结果传递给下一个纹理单位,直到所有纹理单位的操作均完成为止。

多重纹理能够实现一些高级的渲染技巧,例如光照效果、贴花、合成和细节纹理等。

  • 多重纹理的步骤

    (1)对于每个纹理单位,建立相关的纹理状态,包括纹理图像,过滤器、纹理环境、纹理坐标生成和纹理矩阵等。可以使用glActiveTexture()函数更改当前的纹理单位。任何OpenGL的实现必须支持2个纹理单位。

    (2)在指定顶点时,可以使用glMultiTexCoord*()函数为每个顶点指定多个纹理坐标,分别用于不同的纹理单位。每个纹理坐标分别用于每个纹理单位的处理。纹理坐标自动生成以及在顶点数组中指定纹理数组属于特殊情况。

    • 建立纹理单位

      多重纹理引入了纹理单位,用于在应用程序中进行多道纹理处理。每个纹理单位都具有相同的功能,并且具有自己的纹理状态,包括:

      1. 纹理图像
      2. 过滤参数
      3. 纹理环境应用
      4. 纹理坐标自动生成
      5. 顶点数组指定
    • 当 一个需要进行纹理处理的多边形被渲染时,就会在渲染中使用这两个纹理单位。在第一个纹理单位中,当纹理图像texels0使用了最邻近过滤、重复环绕和替 换纹理环境,并根据一个纹理矩阵进行旋转。在第一个纹理单位渲染完成后,这个经过纹理处理的额多边形便发送到第二个纹理单位,在线性过滤、边缘截取、调整 纹理环境以及默认的单位矩阵下应用纹理图像texels1。

      如果使用纹理对象,可以把一个纹理对象绑定到当前纹理单位。当前纹理单位就具有这个纹理对象所包含的纹理状态的值(包括纹理图像)。

      为 了向每个纹理单位分配纹理信息,可以使用glActiveTexture()函数选择需要修改的当前纹理单位。在此之后,对glTexImage*()、 glTexParameter*()、glTexEnv*()、glTexGen*()和glBindTexture()函数的调用就像是查询当前纹理坐 标和当前光栅纹理坐标一样,只影响当前纹理单位。

      每个纹理单位根据它的纹理状态,把原先的片断颜色与纹理图像进行组合。然后把上述操作产生的片断颜色传递给下一个纹理单位(如果后者处于活动状态)。

    • 指定顶点以及它们的纹理坐标

      在多重纹理中,每个顶点只有一组纹理坐标是不够的,需要为每个顶点的每个纹理单位都指定一组纹理坐标。我们不再使用glTexCoord*()函数,而是必须使用glMultiTexCoord*()函数。这个函数除了指定纹理坐标之外,还指定了纹理单位。

      一 种非常少见的情形是对位图或图像矩形进行多重纹理处理,此时需要把每个光栅位置与几个纹理坐标相关联。因此,必须多次调用 glMultiTexCoord*()函数,分别用于每个纹理单位、每个glRasterPos*()和glWindowPos*()调用(由于整个位图 或图像矩阵只有一个当前光栅位置,每个纹理单元只有一个对应的纹理坐标,因此在这种情况下应用多重纹理所增加的美感非常有限)。

    • 指定纹理坐标的其他方法 在使用多重纹理时,显式调用glMultiTexCoord*()函数只是指定纹理坐标的3种方法之一。另两种方法是使用纹理坐标自动生成(glTexGen*()函数)和顶点数组(使用glTexCoordPointer()函数)。

      如果在多重纹理下使用顶点数组来指定纹理坐标,可以使用glClientActiveTexture()函数确定glTexCoordPointer()函数所指定的纹理坐标应用于哪个纹理单位。

    • 恢复使用单个纹理单位 在使用多重纹理时,如果想恢复为使用单个纹理单位,需要禁用除纹理单位0之外的所有纹理单位。

纹理组合器函数

随着OpenGL的演变,它的重心逐渐从原先的顶点处理(变换、裁剪)过渡到光栅化和片断操作。程序接触到的纹理细节越来越多,片断的处理能力也越来越强。

除了多重纹理技术之外,OpenGL还提供了一些灵活的纹理组合器函数,允许程序员对片断与纹理值或其他颜色值的混合施加更加精细的控制。纹理组 合器函数支持高质量的纹理效果,例如凹凸贴图、更灵活的真实镜面光照以及纹理淡出效果(例如对2种纹理进行插值)等。纹理组合器函数可以从3种来源接收颜 色和alpha数据,并对它们进行处理,生成RGBA颜色作为它的输出,用于后续的操作。

可以广泛使用glTexEnv*()函数配置纹理组合器函数。

下面是使用纹理组合器函数的步骤。如果使用的是多重纹理,可以为每个纹理单位使用不同的纹理组合器函数,因此需要对每个纹理单位重复这些步骤。

  • 为了使用纹理组合器函数,必须调用glTexEnvf(GL_TEXTURE_EVN, GL_TEXTURE_EVN_MODE, GL_COMBINE);
  • 指定在纹理组合中怎样使用RGB或alpha值:GL_DOT3_RGB3和GL_DOT3_RGBA模式存在微小的区别,在 GL_DOT3_RGB3模式中,所有3个值设置为同一个计算结果(点积),而在GL_DOT3_RGBA模式下,所有4个值设置为同一个计算结果;
  • 使用GL_SOURCEi_RGB常量指定纹理组合器的第i个参数来自何处。参数的数量(最多为3个)取决于选择的函数类型。当pname是GL_SOURCEi_RGB,pname参数就指定了纹理组合器函数的第i个参数的来源:
    • GL_TEXTURE:第i个参数的来源就是当前纹理单位的纹理;
    • GL_TEXTUREn:与纹理单位n相关联的纹理(如果使用这个来源,纹理单位n必须启用并且有效。否则,其结果就是未定义的);
    • GL_CONSTANT:GL_TEXTURE_ENV_COLOR设置的常量颜色;
    • GL_PRIMARY_COLOR:片断主颜色用于纹理单位0,也就是进行纹理处理之前的片断颜色;
    • GL_PREVIOUS:使用前一个纹理单位的片断(对于纹理单位0,和GL_PRIMARY_COLOR相同)。假设为纹理单位2设置 了GL_SUBTRACT组合器代码,那么计算方法就是把纹理单元2的输出(GL_TEXTURE, arg0)减去纹理单位1的输出(GL_PREVIOUS, arg1);
    • 指定需要使用哪些源值(RGB或alpha)以及如何使用它们;
    • GL_OPERANDi_RGB与对应的GL_SOURCEi_RGB相匹配,并确定当前的GL_COMBING_RGB函数的颜色值;
    • 类似的,GL_OPERANDi_ALPHA与对应的GL SOURCEi ALPHA相匹配,并确定当前的GL_COMBINE_ALPHA函数的颜色值。 当GL_SRC_ALPHA用于GL_COMBINE_RGB函数时,组合器来源的alpha值就解释为R、G、B;
  • 选择RGB或alpha缩放因子(可选);
  • 最后,绘制几何图形,并确保所有的顶点都拥有与之关联的纹理坐标。

  • 插值组合函数

插值函数可以帮助我们演示纹理组合器的用法,因为它使用了最多数量的参数,并使用了几种来源和操作模式。我们获得的插值结果是纹理图像和未进行纹理处理时的片断的加权混合操作的结果。

在纹理之后应用辅助颜色

把一个纹理应用于一个典型的片断时,只有主颜色与纹理单元的颜色进行组合。主颜色可能是光照计算的结果,也可能是由glColor*()函数所指定的。

在纹理处理之后,在执行雾计算之前,可以对片断应用一种辅助颜色。应用辅助颜色可以使经过纹理贴图的物体具有更真实的镜面亮点效果。

  • 在禁用光照时使用辅助颜色

    如果并没有启用光照,并且启用了颜色求和模式(使用glEnable(GL_COLOR_SUM)),那么当前的辅助颜色便添加到经过纹理处理的片断颜色上。

  • 启用光照后的辅助镜面颜色

    纹 理操作时在光照之后进行的,但是把镜面亮点和纹理的颜色进行混合通常会减弱光照的效果。可以为每个顶点计算2种颜色:一种是主颜色,由所有的非镜面成分组 成,另一种是辅助颜色,由所有的镜面成分组成。如果把镜面颜色分离出来,辅助颜色就可以在纹理计算之后再添加到片断中。

点块纹理

虽然OpenGL支持对大小超过1像素的大点(用glPointSize()设置)进行抗锯齿处理,但是它的视觉效果未必能够达到应用程序的要 求。点块纹理允许对大点的着色施加更好的控制。在默认情况下,在启用了点块纹理后,纹理中的每个片断都分配一个相同的状态,与被渲染点的顶点的初始状态相 同。点块纹理通过对这个大点的所有片断的纹理坐标进行迭代,修改片断数据的生成方式。

为了启用点块纹理,可以用GL_POINT_SPRITE为参数调用glEnable()函数。这将导致OpenGL忽略当前的点抗锯齿设置。大点中的每个片断将分配相关的顶点数据,这些值将会在着色中使用。

为了根据点块纹理对纹理坐标进行迭代,需要调用glTexEnv*(GL_POINT_SPRITE, GL_COORD_REPLACE, GL_TRUE)。在多重纹理中,需要为每个需要应用点块纹理的纹理单位都调用这个函数。

点块纹理的纹理坐标是由OpenGL在光栅化过程中自动分配的。

纹理矩阵堆栈

就像物体的坐标在进行渲染之前需要根据矩阵进行变换一样,纹理坐标在用于纹理贴图之前也需要乘以一个4x4的矩阵。在默认情况下,纹理矩阵是单位 矩阵,因此显式指定或自动生成的纹理坐标并不会发生变化。但是,在重绘物体时通过修改纹理矩阵,可以实现使纹理沿表面滑动、绕表面旋转、收缩或放大的效 果,或者上述3种效果的组合。事实上,由于纹理矩阵完全是一个普通的4x4矩阵,因此可以实现诸如透视这样的效果。

纹理矩阵实际上是一个位于堆栈顶部的矩阵,这个堆栈的深度至少能够容纳2个矩阵。所有像glpushMatrix()、 glPopMatrix()、glMultMatrix()和glRotate*()这样的矩阵操纵函数都可以在纹理矩阵上使用。为了更改当前的纹理矩 阵,需要把矩阵模式设置为GL_TEXTURE。

  • q坐标

    第4个纹理坐标q的数学意义相当于物体坐标的w坐标。当4个纹理坐标(s, t, r, q)与纹理矩阵相乘时,它所产生的向量(s', t', r', q')解释为齐次纹理坐标。换句话说,纹理图像是通过坐标值s'/q'、t'/q'和r'/q'进行索引的。

    如 果需要多个投影或透视变换,可能就要用到q坐标。例如,如果想对强度不匀(例如中间更亮,或者由于灯罩或镜头的影响使光束为非圆锥形)的聚光灯进行建模。 为了模拟光线照射到平面上的情况,可以创建一个与光线形状和强度相对应的纹理,然后使用投影变换把纹理投影到平面上。当我们在场景中把光锥投影到平面上 时,需要使用透视变换(q不等于1),因为光线可能并不垂直于被照射的表面。当观察者从一个不同的观察点观察场景(使用透视方式)时,就可能出现第二个透 视变换。

    使用q坐标的另一个例子是当纹理图像是以透视方式拍摄的照片时。和聚光灯一样,最终的视图取决于2个投影变换的组合。

深度纹理

当一个表面接受光照之后,我们会注意到OpenGL光源并没有在表面上投射出阴影。每个顶点的颜色在计算时并没有考虑场景中的其他物体。为了产生阴影,需要确定并记录哪些表面和光源的直线路径上具有遮挡物。

一种使用深度纹理进行多道渲染的技巧可以提供一种渲染阴影的解决方案。如果把观察点临时移到光源位置,我们就会注意到自己所看到的一切都被光所照 射,也就是从这个角度观察不存在阴影。深度纹理提供了一种机制,把所有“无阴影”的片断与阴影图中的对应深度值进行比较,就可以根据每个片断是否罩有阴影 来选择如何对它进行渲染。这种思路类似于深度测试,只不过它是把光源作为观察点来进行操作的。

(1)从光源角度对场景进行渲染。场景看上去是什么样子无关紧要,只需要深度值。然后创建阴影图,也就是捕捉深度缓冲区的值,并把它们存储在一个纹理图像(即阴影图)种。

(2)生成纹理坐标,其中(s, t)坐标引用阴影图中的位置,第三个纹理坐标r则表示与光源的距离。然后再次绘制场景,把每个片断的r值与对应的深度纹理值进行比较,以确定这个片断是被光所照射还是被阴影所笼罩。

  • 创建阴影图

    第一个步骤是创建包含深度值的阴影图。可以把观察点设置为光源的位置,然后对场景进行渲染,从而创建这个阴影图。样例使用了glGetLightfv()函数获取当前光源位置,并计算一个朝上向量,然后用它进行视图变换。

    首先设置视口大小,使之与阴影图的大小相匹配。然后,它设置适当的投影和视图矩阵。场景中的物体被渲染,所产生的深度图像被复制到纹理内存中,作为阴影图使用。最后,视口被重置为原先的大小和位置。需要注意以下要点:

    • 投影矩阵控制光源“灯罩”的形状。gluPerspective()函数中的变量lightFovy和lightAspect用于控制这个灯罩的大小。lightFovy值越小,光源就越像聚光灯。lightFovy越大,光源就越像泛光灯。
    • 光源的近侧和远侧裁剪平面用于控制深度值的精度。应该尽量使近侧和远侧裁剪平面之间的距离保持很小,以提高深度值的精度。
    • 在深度缓冲区中保存了深度值之后,需要捕捉它们,并把它们放在一个GL_DEPTH_COMPONENT格式的纹理图像中。
  • 生成纹理坐标并进行渲染

    现在,可以使用glTexGen*()函数自动生成纹理坐标,计算与光源的视觉空间距离。r坐标的值对应于图元与光源的距离。为此,可以使用和创建与阴影图相同的投影和视图变换。样例使用了GL_MODELVIEW矩阵堆栈执行所有的矩阵计算。

    注意,生成的(s, t, r, q)纹理坐标和阴影图中的深度值并没有进行类似的缩放。纹理坐标是在视觉坐标下产生的,因此它们的范围位于[-1, 1]。纹理单元的深度值的范围位于[0, 1]。因此,应该进行移动和缩放,把纹理坐标的值映射到和阴影图相同的范围。

    在 样例中,在场景的第二次和最后一次渲染之前,纹理比较模式GL_COMPARE_R_TO_TEXTURE指示OpenGL把片断的r坐标与纹理单元值进 行比较。如果r的距离小于或等于(比较函数为GL_LEQUAL)纹理单元值,这个片断和光源之间就没有其他物体,它就是被光源直接照射的,其有效亮度值 为1。如果比较结果失败,那么这个片断和光源之间还存在其他图元,因此这个片断就被阴影所笼罩,它的有效亮度值为0。

    这种技巧也可能产生意想不到的人工视觉效果:

    • 自身阴影,也就是一个物体不正确地把阴影投影到自身,这是一个常见的问题;
    • 可能会出现投影纹理的锯齿效果,特别是那些离光源非常远的区域。使用高分辨率的阴影图可以帮助减少这种情况;
    • GL_MODULATE模式应用到深度纹理时会导致阴影和非阴影区域之间的急剧过渡。
  • 遗憾的是,这些问题并没有通用而有效的解决方法。只能多次试验产生具有最佳效果的图像。
0 0
原创粉丝点击