通常来讲,计算机图形学的目标是计算一张图片上的每个组成部分的颜色,虽然我们可以通过着色器中的算法来计算像素的颜色,不过很多时候这种着色器的实现过程太过复杂,不适合实际应用。这种时候我们可以选择使用纹理----它是由大块的图像数据组成的,可以用来绘制到物体的表面以增强其真实感。OpenGL也没有对“纹理”这个概念做更多的介绍:作为个人来讲,一个纹理其实就是一幅图像,我们可以把这幅图像的整体或部分贴到我们先前用顶点勾画出的物体上去——比如对一个立方体、圆等贴上纹理图,使这个物体看起来更加的真实。
纹理映射
在物理世界中,视域内的颜色会发生快速的变化,例如你坐在你家里的沙发上, 你可以观察到房间里的墙壁,天花板,地板以及房间里的其他物品,除非这个房间没有任何物体,否则你将会看到很多物体的表面上都会呈现出丰富的颜色,并且在狭小的面积上产生多彩的变化。要捕捉细节如此丰富的色彩变化是非常辛苦和缜密的工作(你需要有效地辨别每个线性色彩变化区域中的每个三角形)。如果能找到一张图片,然后把它“粘”到物体表面上,就像贴墙纸一样,那就简单多了,这就是纹理映射(texture mapping)。通常来说,纹理贴图(或者简称为纹理)是由摄像机拍摄或者艺术家绘制的一张图片。在自然界中的纹理是二维的,但是OpenGL也支持其他类型的纹理格式:一维纹理,二维纹理,三维纹理,立方体映射纹理,以及缓存纹理。同时还支持纹理数组。纹理是由纹素(texel)组成的,其中通常包含颜色数据信息。
以左下角为原点,向右伸展到1.0的位置,向上伸展到1.0的位置,表示一整张的纹理图像
图片的存储空间
图像存储空间 = 图像的宽度width * 图像高度height * 每个像素的字节数
纹理相关函数
1.改变像素的存储方式
改变像素的存储方式:
void glPixelStorei(GLenum pname,GLint param);
恢复像素存储方式:
void glPixelStoref(GLenum pname,GLfloat param);
正常来说,我们是没必要来改变像素的存储方式的,以上两个函数仅仅作为了解。
这两个函数的功能是一样的,只不过函数名一个是 i 结尾,一个是 f 结尾,区别只是第二个参数GLint param的类型,i 的是 GLint,f 的是 GLfloat。
glPixelStorei(GL_UNPACK_ALIGNMENT,1);
参数1:GL_UNPACK_ALIGNMENT,指定 OpenGL 如何从数据缓冲区中解包图像数据。
参数2:针对 GL_UNPACK_ALIGNMENT 设置的值。
这里pname参数的符号有两种:一种是GL_PACK_ALIGNMENT,它影响将像素数据写回到主存的打包形式,对glReadPixels的调用产生影响。
还有一种是GL_UNPACK_ALIGNMENT,它影响从主存读到的像素数据的解包形式,对glTexImage2D以及glTexSubImage2D产生影响。
GL_UNPACK_ALIGNMENT 指内存中每个像素⾏起点的排列列请求,允许设置为:1 (byte排列)、2(排列为偶数byte的⾏)、4(字word排列)、8(⾏从双字节边界开始),对齐的字节数越高,系统就越能优化。
2.将颜色缓存区的内容作为像素图直接读取
void glReadPixels(GLint x,GLint y,GLSizei width,GLSizei height, GLenum format, GLenum type,const void * pixels);
参数1:x,矩形左下角的窗⼝坐标
参数2:y,矩形左下角的窗⼝坐标
参数3:width,矩形的宽,以像素为单位
参数4:height,矩形的高,以像素为单位
参数5:format,OpenGL 的像素格式
参数6:type,解释参数 pixels 指向的数据,告诉OpenGL 使⽤缓存区中的什么数据类型来存储颜⾊分量,像素数据的数据类型
参数7:pixels,指向图形数据的指针
void glReadBuffer(GLenum mode);
—> 指定读取的缓存
void glWriteBuffer(GLenum mode);
—> 指定写⼊的缓存
下面来看一下OpenGL中的像素格式有哪些:如下表所示
常量 | 描述 |
---|---|
GL_RED | 每个像素只包含了一个红⾊分量 |
GL_GREEN | 每个像素只包含了一个绿色分量 |
GL_BLUE | 每个像素只包含了一个蓝⾊分量 |
GL_RG | 每个像素依次包含了一个红⾊和绿⾊的分量 |
GL_RGB | 描述红、绿、蓝顺序排列的颜⾊ |
GL_RGBA | 按照红、绿、蓝、Alpha顺序排列的颜色 |
GL_BGR | 按照蓝、绿、红顺序排列颜色 |
GL_BGRA | 按照蓝、绿、红、Alpha顺序排列颜色 |
GL_RED_INTEGER | 每个像素包含了一个整数形式的红色分量 |
GL_GREEN_INTEGER | 每个像素包含了一个整数形式的绿色分量 |
GL_BLUE_INTEGER | 每个像素包含了一个整数形式的蓝色分量 |
GL_RG_INTEGER | 每个像素依次包含了一个整数形式的红色、绿色分量 |
GL_RGB_INTEGER | 每个像素包含了一个整数形式的红色、蓝色、绿色分量 |
GL_RGBA_INTEGER | 每个像素包含了一个整数形式的红色、蓝色、绿色、Alpah分量 |
GL_BGR_INTEGER | 每个像素包含了一个整数形式的蓝色、绿色、红⾊分量 |
GL_BGRA_INTEGER | 每个像素包含了一个整数形式的蓝色、绿色、红色、Alpah分量 |
GL_STENCIL_INDEX | 每个像素只包含了⼀个模板值 |
GL_DEPTH_COMPONENT | 每个像素值包含⼀个深度值 |
GL_DEPTH_STENCIL | 每个像素包含一个深度值和⼀个模板值 |
在上述表格中的最后3个格式 GL_STENCIL_INDEX、GL_DEPTH_COMPONENT 和 GL_DEPTH_STENCIL 用于对模板缓冲区和深度缓冲区直接进行读写。
下面再来看一下像素数据的数据类型有哪些?如下表:
常量 | 描述 |
---|---|
GL_UNSIGNED_BYTE | 每种颜色分量都是一个8 位无符号整数 |
GL_BYTE | 每种颜色分量都是一个8 位有符号整数 |
GL_UNSIGNED_SHORT | 每种颜色分量都是一个16 位无符号整数 |
GL_SHORT | 每种颜色分量都是一个16 位有符号整数 |
GL_UNSIGNED_INT | 每种颜色分量都是一个32 位无符号整数 |
GL_INT | 每种颜色分量都是一个32 位有符号整数 |
GL_FLOAT | 每种颜色分量都是一个单精度浮点数 |
GL_HALF_FLOAT | 每种颜色分量都是一个半精度浮点数 |
GL_UNSIGNED_BYTE_3_2_2 | 包装的 RGB 值 |
GL_UNSIGNED_BYTE_2_3_3_REV | 包装的 RGB 值 |
GL_UNSIGNED_SHORT_5_6_5 | 包装的 RGB 值 |
GL_UNSIGNED_SHORT_5_6_5_REV | 包装的 RGB 值 |
GL_UNSIGNED_SHORT_4_4_4_4 | 包装的 RGB 值 |
GL_UNSIGNED_SHORT_4_4_4_4_REV | 包装的 RGB 值 |
GL_UNSIGNED_SHORT_5_5_5_1 | 包装的 RGB 值 |
GL_UNSIGNED_SHORT_5_5_5_1_REV | 包装的 RGB 值 |
GL_UNSIGNED_INT_8_8_8_8 | 包装的 RGB 值 |
GL_UNSIGNED_INT_8_8_8_8_REV | 包装的 RGB 值 |
GL_UNSIGNED_INT_10_10_10_2 | 包装的 RGB 值 |
GL_UNSIGNED_INT_2_10_10_10_REV | 包装的 RGB 值 |
GL_UNSIGNED_INT_24_8 | 包装的 RGB 值 |
GL_UNSIGNED_INT_10F_11F_11F_REV | 包装的 RGB 值 |
GL_FLOAT_32_UNSIGNED_INT_24_8_REV | 包装的 RGB 值 |
3. 载入纹理函数
在读取完一张纹理贴图之后,需要把它载入到纹理中去,一旦被载入,这些纹理就会成为当前纹理状态的一部分。
void glTexImage1D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLint border, GLenum format, GLenum type, const GLvoid *pixels);
void glTexImage2D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid *pixels);
void glTexImage3D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type, const GLvoid *pixels);
实际上,这三个函数是由函数 glTexImage 派生出来的,在开发过程中,我们在使用载入纹理时,使用的glTexImage2D这个函数。
参数target:纹理维度GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
参数level:指定所加载的mip贴图层次,一般我们都把这个参数设置为0
参数internalformat:每个纹理理单元中存储多少颜⾊色成分。(从读取像素图时获得)
参数width:指加载纹理的宽度
参数height:指加载纹理的高度
参数depth:指加载纹理的深度
参数border :允许为纹理理贴图指定⼀一个边界宽度。
参数format :像素数据的数据类型(GL_UNSIGNED_BYTE,每个颜色分量都是一个8位无符号整数)
参数type :解释参数 pixels 指向的数据,告诉OpenGL 使⽤缓存区中的什么数据类型来存储颜⾊分量,像素数据的数据类型
参数*pixels :指向纹理图像数据的指针
除此之外,还可以通过颜色缓冲区载入纹理数据,
void glCopyTexImage1D(GLenum target,GLint level,GLenum
internalformt,GLint x,GLint y,GLsizei width,GLint border);
void glCopyTexImage2D(GLenum target,GLint level,GLenum
internalformt,GLint x,GLint y,GLsizei width,GLsizei
height,GLint border);
这俩函数的类似于 glTexImage,但在这里参数 x 和 参数y 在颜色缓冲区中指定了开始读取纹理数据的位置。源缓冲区是通过 glReadBuffer 函数设置的。并不存在 glCopyTexImage3D,因为我们无法从 2D 颜色缓冲区获取体积数据。
4.更新纹理
void glTexSubImage1D(GLenum target,GLint level,GLint xOffset,GLsizei width,GLenum
format,GLenum type,const GLvoid *data);
void glTexSubImage2D(GLenum target,GLint level,GLint xOffset,GLint yOffset,GLsizei
width,GLsizei height,GLenum format,GLenum type,const GLvoid *data);
void glTexSubImage3D(GLenum target,GLint level,GLint xOffset,GLint yOffset,GLint
zOffset,GLsizei width,GLsizei height,GLsizei depth,Glenum type,const GLvoid * data);
与glTexImage函数不同的是:xOffset、yOffset 和 zOffset 参数指定了在原来的纹理贴图中开始替换纹理数据的偏移量。width、height 和 depth 参数指定了插入到原来那个纹理中的新纹理的宽度、高度和深度
5.插入替换纹理
void glTexSubImage1D(GLenum target,GLint level,GLint xOffset,GLsizei width,GLenum
format,GLenum type,const GLvoid *data);
void glTexSubImage2D(GLenum target,GLint level,GLint xOffset,GLint yOffset,GLsizei
width,GLsizei height,GLenum format,GLenum type,const GLvoid *data);
void glTexSubImage3D(GLenum target,GLint level,GLint xOffset,GLint yOffset,GLint
zOffset,GLsizei width,GLsizei height,GLsizei depth,Glenum type,const GLvoid * data);
值得注意的是:这里并没有 glCopyTexImage 函数。这是因为颜色缓冲区是 2D的,不存在一种对应的方法来将一个2D 彩色图像作为一个 3D 纹理的来源。但是,我们可以使用 glCopyTexSubImage3D 函数,在一个三维纹理中使用颜色缓冲区的数据来设置它的一个纹理单元平面。
6.纹理对象
1. 分配纹理对象
void glGenTextures (GLsizei n, GLuint *textures);
这个glGenTextures函数需要指定纹理对象的数量和指针,这个指针指向一个无符号整形数组,由纹理对象标识填充
2. 绑定纹理状态
void glBindTexture (GLenum target, GLuint texture);
参数target: GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
参数texture:需要绑定的纹理对象
当纹理状态被绑定后,在以后的纹理加载和纹理参数设置只会影响当前绑定的纹理对象
3. 删除绑定的纹理对象
void glDeleteTextures (GLsizei n, const GLuint *textures);
glDeleteTextures删除纹理对象函数需要传入对应的纹理对象以及纹理对象的指针,指针指向的是一个无符号整形数组,由纹理对象标识符填充
4. 测试纹理对象是否有效
GLboolean glIsTexture(GLuint texture)
如果texture是一个已经分配空间的纹理对象,那么这个函数会返回GL_TRUE,否则返回GL_FALSE
7.设置纹理参数
void glTexParameterf (GLenum target, GLenum pname, GLfloat param);
void glTexParameterfv (GLenum target, GLenum pname, const GLfloat *params);
void glTexParameteri (GLenum target, GLenum pname, GLint param);
void glTexParameteriv (GLenum target, GLenum pname, const GLint *params);
参数target::指定这些参数将要应⽤在哪个纹理模式上,⽐如 GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D。(一般就设设置为GL_TEXTURE_2D)
参数pname:指定了需要设置哪个纹理参数
参数param 或 params:用于设置特定的纹理参数的值
1. 设置纹理的过滤方式
根据一个放大或缩小的纹理贴图计算颜色片段的过程称为纹理过滤,在纹理参数时,可以同时设置放大和缩小过滤器,这两种过滤器的参数名分别是 GL_TEXTURE_MAG_FILTER 和 GL_TEXTURE_MIN_FILTER。我们可以为它们从两种基本的纹理过滤器 GL_NEAREST 和 GL_LINEAR 中进行选择,它们分别对应于邻近过滤和线性过滤。
邻近过滤GL_NEAREST
邻近过滤是把最邻近的纹理单元应用到纹理坐标中,指的是纹理坐标最靠近哪个纹素,就用哪个纹素(这是OpenGL默认的过滤方式,速度最快,但是效果最差)
上图中的“+”号代表纹理像素的坐标,那么这个像素点再读取纹理的时候会取它邻近的颜色值。
线性过滤
线性过滤会把这个纹理坐标周围的纹理单元的加权平均值应用到这个纹理坐标上(线性插值),一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。(这是OpenGL应用最广泛的一种方式,效果一般,速度较快)
很明显,邻近过滤的像素痕迹非常明显,一块一块的。而线性过滤的方式效果就好上很多了,虽然感觉很模糊,但我们完全能理解一张小图放大之后会模糊这件事。
可以用下面这个函数来设置过滤方式:
void glTexParameteri (GLenum target, GLenum pname, GLint param);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); //缩小时的邻近过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //放大时的线性过滤方式
四个组合方式的过滤:
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_HEAREST);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_HEAREST);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
2. 设置纹理的环绕方式
通常,纹理坐标的范围在(0,0)到(1,1)之间,使它与纹理贴图中的纹理单元形成映射关系。但是如果我们制定的坐标在这之外呢?默认情况下,OpenGL会重复绘制纹理图,不过与此同时OpenGL也提供了更多的环绕方式:
环绕方式 | 描述 |
---|---|
GL_REPEAT | 对纹理的默认行为,重复纹理图像 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一样,但每次重复图片是镜像放置的 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果 |
GL_CLAMP_TO_BORDER | 超出的坐标为用户指定的边缘颜色 |
下面来看一下这四种环绕方式的效果:
设置纹理环绕方式的方法是调用glTexParameteri函数,具体方式如下:
//横坐标
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
//纵坐标
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
参数1:纹理维度。
GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
参数2:为S/T坐标设置模式。
GL_TEXTURE_WRAP_S、GL_TEXTURE_T、GL_TEXTURE_R,针对s,t,r坐标
参数3:wrapMode,环绕模式。
GL_REPEAT、
GL_CLAMP、
GL_CLAMP_TO_EDGE、GL_CLAMP_TO_BORDER
(1) GL_REPEAT: OpenGL 在纹理坐标超过1.0的⽅向上对纹理理进⾏重复;
(2) GL_CLAMP:所需的纹理单元取自纹理边界或TEXTURE_BORDER_COLOR.
(3) GL_CLAMP_TO_EDGE:环绕模式强制对范围之外的纹理坐标沿着合法的纹理单元的最后一⾏或者最后一列来进行采样。
(4) GL_CLAMP_TO_BORDER:在纹理坐标在0.0到1.0范围之外的只使⽤边界纹理单元。边界纹理单元是作为围绕基本图像的额外的行和列,并与基本纹理图像⼀起加载的。
至于当设定了GL_CLAMP_TO_BORDER的环绕方式,想要指定边界颜色,就需要使用glTexParameterfv函数了,像这样:
float borderColor[] = { 0.0f, 1.0f, 0.0f, 1.0f }; //指定成绿色
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
设置颜色需要在设置了环绕方式之后使用。