系列推荐文章:
OpenGL/OpenGL ES入门:图形API以及专业名词解析
OpenGL/OpenGL ES入门:渲染流程以及固定存储着色器
OpenGL/OpenGL ES入门:图像渲染实现以及渲染问题
OpenGL/OpenGL ES入门:基础变换 - 初识向量/矩阵
OpenGL/OpenGL ES入门:纹理初探 - 常用API解析
OpenGL/OpenGL ES入门: 纹理应用 - 纹理坐标及案例解析(金字塔)
什么是纹理
在之前的几片文章中,已经对点、线和三角形进行了渲染,也看到了如何通过计算颜色值对它们进行着色,以及在它们之间进行值操作来模拟光照效果。为了能够达到更加真实的效果,这一篇引入纹理贴图。
纹理只是一种能够应用到场景中的三角形上的图像数据,它通过经过过滤的纹理单元(texel
,相当于基于纹理的像素)填充到实心区域。
初识纹理的小伙伴们可以理解为,纹理就是图片。当然纹理远远不止是图像数据这么简单,它是大多数现代3D渲染算法的一个关键因素。这里只做简单了解。
像素
像素包装
图像数据在内存中很少以紧密包装的形式存在。在许多硬件平台上,处于性能考虑,一幅图像的每一行都应该从一种特定的字节对齐地址开始。绝大多数编译器会自动把变量和缓冲区放置在一个针对该架构对齐优化的地址上。
默认情况下,OpenGL
采用4个字节的对齐方式,这种方式适合于很多目前正在使用时的系统。
下面这个句话引用来自《OpenGL超级宝典》(第5版)
很多程序员会简单地将图像宽度值乘以高度值,在乘以每个像素的字节数,这样就错误地判断一个图像所需的存储器数量。
例如:一幅RGB图像,包含3个分量,每个分类都存储在一个字节中(每个颜色通道8位),如果图像的宽度为199个像素,那么图像的每一行需要多少存储空间呢?
按照上面的算法来计算:199*3 = 597字节
这样也许是对的,但是作为优秀的程序员,可能会讨厌这个数字。如果硬件本身的体系结构是4字节排列(大部分是这样的),那么图像每一行的末尾都将有额外的3个空字节进行填充(每一行600字节),而这是为了使每一行的存储器地址从一个能够被4整除的地址开始。
许多未经压缩的图像文件格式也遵循这种惯例,然而Targa(.TGA)
文件格式则是1个字节排列的,这样不会浪费空间。为什么内存分配意图对于OpenGL
来说这么重要?
因为在我们想OpenGL
提交图像数据或从OpenGL
获取图像数据时,OpenGL
需要知道我们想要在内存中对数据进行怎样的包装或解包装操作。
认识一下下面几个函数:
// 改变像素存储方式
glPixelStorei(GLenum pname, GLint param);
// 恢复像素存储值方式
glPixelStoref(GLenum pname, GLint param);
// 如果我们想要改成紧密包装像素数据,应该像下面这样调用函数
/*
参数1: 指定OpenGL如何从数据缓冲区中解包图像数据
参数2: 允许设置1(byte排列)、2(排列为偶数byte的行)、4(字word排列)、8(行从双字节边界开始)
*/
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
像素图
像素图在内存布局上与位图非常相似,但是每个像素将需要一个以上的存储位来表示。每个像素的附加位允许存储强度(亮度)或者颜色分量值。
在OpenGL
中,可以使用下面的函数将颜色缓冲区的内容作为像素图直接读取。
// 将颜色缓冲区的内容作为像素图直接读取
/*
参数1&参数2: x,y矩形左下角的窗口坐标
参数3&参数4: width,height矩形的宽高,以像素为单位
参数5: 像素格式
参数6: 解释参数pixels指向的数据,告诉OpenGL使用缓冲区中的什么数据类型来存储颜色分量,像素数据的数据类型
参数7: pixels,指向图像数据的指针
*/
glReadPixel(GLint x, GLint y, GLSizei width, GLSizei height, GLenum format, GLenum type, const void *pixels);
/*
模式参数:GL_FRONT、GL_BACK、GL_LEFT、GL_RIGHT、GL_FRONT_LEFT、
GL_FRONT_RIGHT、GL_BACK_LEFT、GL_BACK_RIGHT或者甚至是GL_NONE中的任意一个
*/
// 指定读取的缓存
glReadBuffer(mode);
// 指定写入的缓存
glWriteBuffer(mode);
读取像素
Targa
图像格式是一种方便而且容易使用的图像格式,并且它既支持简单颜色图像,也支持带有Alpha
值的图像。后面篇幅中一致使用这种格式来进行纹理操作。
/*
参数1: 将要载入的Targa文件的文件名
参数2: 文件宽度地址
参数3: 文件高度地址
参数4: 文件数据格式地址
参数5: 文件格式地址
返回值: 如果函数调用成功,返回一个新定位到直接从文件中读取的图像数据的指针,否则返回NULL
*/
GLbyte *gltReadTGABits(const char *szFileName, GLint *iWidth, GLint *iHeight, GLint *iComponents, GLenum *eFormat);
载入纹理
在几何图形中应用贴图时,第一个必要步骤就是将纹理载入内存。一旦被载入,这些纹理就会成为当前纹理状态的一部分。
/*
参数1: GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
参数2: 指定这个函数所加载的mip贴图层次,默认设为0
参数3: 每个纹理单元存储多少颜色成分(从读取像素图时获得)
参数4: width、height、depth指加载纹理的宽度、高度、深度
参数5: 允许为纹理贴图指定一个边界宽度,目前来说,设置为0
参数6: OpenGL 数据存储方式,一般使用GL_UNSIGNED_BYTE
参数7: 图片数据指针
*/
void glTexImage1D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLint border, GLenum format, GLenum type, void *data);
void glTexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, void *data);
void glTexImage3D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type, void *data);
使用颜色缓冲区
一维和二维纹理也可以从颜色缓冲区加载数据。可以从颜色缓冲区读取一幅图像,并通过下面的函数将它作为一个新纹理使用
void glCopyTexImage1D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLint border);
void glCopyTexImage1D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLsizei height, GLint border);
这两个函数的操作类似glTexImage
,但是这里x
和y
在颜色缓冲区中指定了开始读取纹理数据的位置。源缓冲区时通过glReadBuffer
函数设置的。请注意,并不存在glCopyTexImage3D
,因为我们无法从2D颜色缓冲区获取体积数据。
更新纹理
在时间敏感的场合如游戏或模拟应用程序中,重复加载新纹理可能会成为性能瓶颈。
如果我们不再需要某个已加载的纹理,它可以被全部替换,也可以被替换掉一部分。替换一个纹理图像常常要比直接使用glTexImage
重新加载一个新纹理快的多。函数代码如下
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 format, GLenum type, const GLvoid *data);
上面函数绝大部分参数都与glTexImage
函数所使用的参数准确对应。xOffset、yOffset、zOffset
参数指定了在原来的纹理贴图中开始替换纹理数据的偏移量。width、height、depth
参数指定了“插入”到原来那个纹理中的新纹理的宽度、高度和深度。
而下面一组函数允许我们从颜色缓冲区读取纹理,并插入或替换原来纹理的一部分,都是glCopyTexSubImage
函数的变型。
void glCopyTexSubImage1D(GLenum target, GLint level,
GLint xOffset,
GLint x, GLint y,
GLsizei width);
void glCopyTexSubImage2D(GLenum target, GLint level,
GLint xOffset, GLint yOffset
GLint x, GLint y,
GLsizei width, GLsizei height);
void glCopyTexSubImage1D(GLenum target, GLint level,
GLint xOffset, GLint yOffset, GLint zOffset,
GLint x, GLint y,
GLsizei width, GLsizei height);
前面说到,不存在一种对应方法来将一幅2D彩色图像作为一个3D纹理的来源。但是,我们可以使用glCopyTexSubImage3D
函数,在一个三维纹理中使用颜色缓冲区的数据来设置它的一个纹理单元平面。
纹理对象
纹理对象允许我们一次加载一个以上纹理状态(包含纹理图像)。以及在它们之间进行快速切换。纹理状态是由当前绑定的纹理对象维护的。而纹理对象时一个无符号整数标识的。
//使用函数分配纹理对象
//指定纹理对象的数量 和 指针(指针指向一个无符号整形数组,由纹理对象标识符填充)。
void glGenTextures(GLsizei n, GLuint *textTures);
//绑定纹理状态
//参数1: GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
//参数2: 需要绑定的纹理对象
void glBindTexture(GLenum target, GLunit texture);
//删除绑定纹理对象
//纹理对象 以及 纹理对象指针(指针指向一个无符号整形数组,由纹理对象标识符填充)。
void glDeleteTextures(GLsizei n, GLuint *textures);
//测试纹理对象是否有效
//如果texture是一个已经分配空间的纹理对象,那么这个函数会返回GL_TRUE,否则会返回GL_FALSE。
GLboolean glIsTexture(GLuint texture);
纹理参数设置
和将一幅图片贴在三角形的一面相比,纹理贴图需要更多的工作,很多参数的应用都会影响渲染的规则和纹理贴图的行为。这些纹理参数都是通过glTexParameter
函数的变量来进行设置的。
/*
参数1: target,指定这些参数将要应用在那个纹理模式上,比如GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D。
参数2: pname,指定需要设置那个纹理参数
参数3: param,设定特定的纹理参数的值
*/
glTexParameterf(GLenum target, GLenum pname, GLFloat param);
glTexParameteri(GLenum target, GLenum pname, GLint param);
glTexParameterfv(GLenum target, GLenum pname, GLFloat *param);
glTexParameteriv(GLenum target, GLenum pname, GLint *param);
基本过滤
根据一个拉伸或收缩的纹理贴图计算颜色片段的过程称为纹理过滤。
使用OpenGL
的纹理参数函数,可以同时设置放大和缩小过滤器。参数名为:GL_TEXTURE_MAG_FILTER
和GL_TEXTURE_MIN_FILTER
。
就目前来说,可以认为它们从两种基本的纹理过滤器:最邻近过滤(GL_NEAREST)和线性过滤(GL_LINEAR)中选择。
最邻近过滤: 最为显著的特征就是当纹理被拉伸到特别大时,所出现的大片斑驳像素。它是我们能够选择的最简单、最快速的过滤方法。
线性过滤: 最为显著的特征就是当纹理被拉伸时,所出现的“失真”图形,但是,和最邻近过滤模式下所呈现的斑驳状像素块相比较,这种“失真”更接近事实。
/*
参数1: 纹理维度
参数2: 放大&缩小过滤器
参数3: 环绕模式
*/
// 为放大和缩小过滤器设置纹理过滤器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEARST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEARST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
通过下面的图片可以比较一下两种过滤的区别:
纹理环绕
在正常情况下,是在0.0到1.0的范围之内指定纹理坐标,使它与纹理贴图中的纹理单元形成映射关系。如果纹理坐标落在这个范围之外,OpenGL
则根据当前纹理环绕模式处理这个问题。
调用glTexParameter
函数(并分别使用GL_TEXTURE_WRAP_S、GL_TEXTURE_WRAP_T或GL_TEXTURE_WRAP_R
做参数),为每个坐标分别设置环绕模式。
/*
参数1: 纹理维度 GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
参数2: GL_TEXTURE_WRAP_S、GL_TEXTURE_T、GL_TEXTURE_R,针对s,t,r坐标
参数3: 环绕方式
*/
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);