目录
纹理贴图是在光栅化的模型表面上覆盖图像的技术。
纹理贴图非常重要,因此硬件也为它提供了支持,使得它具备了实现实时的照片级真实感的超高性能。
纹理单元
是【专为】纹理设计的【硬件】组件,现代【显卡】通常带有【数个纹理单元】。
加载纹理图像文件
为了在 OpenGL/GLSL 中有效地完成纹理贴图,需要协调好以下几个不同的数据集和机制:
- 用于保存纹理图像的
纹理对象
(在本章中我们仅考虑 2D 图像); - 一个特殊的统一
采样器
变量,以便顶点着色器可以访问纹理; - 用于保存
纹理坐标
的缓冲区; - 用于将
纹理坐标
传递给管线的顶点属性; - 显卡上的
纹理单元
。
为了使纹理图像可以被用于 OpenGL 管线中的着色器,我们需要从图像中提取颜色并将它们放入 OpenGL 纹理对象(用于保存纹理图像的内置 OpenGL 结构)中。
许多 C++库可用于读取和处理图像文件, 常见的选择包括 Cimg、 BoostGIL 和 Magick++。我们选择使用专为 OpenGL 设计的名为 SOIL2 的库——一个小型的C库,主要用于将纹理上传到OpenGL中。
将纹理加载到 OpenGL 应用程序的步骤是:
(1)使用 SOIL2 实例化 OpenGL 纹理对象并从图像文件中读入数据;
(2)调用 glBindTexture()
绑定纹理对象(比如将纹理对象绑定到2D纹理);
(3)使用 glTexParameter()
函数调整纹理设置。
最终获得的结果:一个可用的OpenGL纹理对象的整型 ID。
SOIL_load_OGL_texture()
函数接受图像文件名作为其参数之一(稍后将描述一些其他参数)。这些步骤在以下函数中实现:
GLuint loadTexture(const char* textImagePath) {
GLuint textureID;
textureID = SOIL_load_OGL_texture(textImagePath, SOIL_LOAD_AUTO, SOIL_CREATE_NEW_ID, SOIL_FLAG_INVERT_Y);
if (textureID == 0)
cout << "could not find texture file" << textImagePath << endl;
return textureID;
}
通过指定SOIL_FLAG_INVERT_Y
参数(invert /ɪnˈvɜːrt/ 颠倒),垂直翻转图像来重新定向,使其与OpenGL的预期格式相对应。因为图像坐标是左下角为原点(0,0),而窗口坐标是左上角为原点(0,0)。
纹理坐标
通过为模型中的每个顶点指定纹理坐标
来完成将纹理应用于对象的渲染表面。
【纹理坐标】是对纹理图像(通常是2D)中的【像素的引用】。
纹理图像中的像素被称为
纹素(Texel)
,以便将它们与在屏幕上呈现的像素区分开。
纹理坐标用于将 3D 模型上的点映射到纹理中的位置。除了将它定位在 3D 空间中的(x,y,z)坐标之外,模型表面上的每个点还具有纹理坐标(s,t),用来指定纹理图像中的哪个纹素为它提供颜色。这样,物体的表面被按照纹理图像“涂画”。纹理在对象表面上的朝向由分配给对象顶点的纹理坐标来确定。
为了确保渲染模型中的【每个像素】都使用纹理图像中的适当【纹素】进行绘制,纹理坐标也需要被放入[顶点属性]中,以便它们也由光栅着色器进行插值。以这种方式,[纹理图像]与[模型顶点]一起被插值或者填充。
我们将设置两个缓冲区,一个用于顶点(x,y,z),另一个用于相应的纹理坐标(s,t)。这样,每次顶点着色器的调用接收到一个顶点的数据,现在包括了其空间坐标和相应的纹理坐标。
2D纹理图像被设定为矩形:左下角(0,0),右上角(1,1)。理想情况下,纹理坐标应该在[0…1]范围内取值。
顶点之间的所有中间像素都已使用图像中间插值的纹素进行绘制。这正是因为纹理坐标在顶点属性中被发送到片段着色器,因此也像顶点本身一样被插值。
3D模型上的每个顶点与纹理坐标一一对应,由纹素给顶点提供颜色。
下图展示了顶点与纹理坐标的一一对应关系:
不过,可以看到图像看起来略微拉伸——这是因为纹理图像的长宽比与立方体面相关的给定纹理坐标的长宽比不匹配。
渲染金字塔模型,只是这次用砖的图像添加纹理。我们需要指定:
(a)引用纹理图像的整型ID;
(b)模型顶点的纹理坐标;
(c)用于保存纹理坐标的缓冲区;
(d)顶点属性,以便顶点着色器可以接收并通过管线转发纹理坐标;
(e)显卡上用于保存纹理对象的纹理单元
;
(f)用于访问 GLSL 中纹理单元的统一采样器变量
。
创建纹理对象 & 构建纹理坐标
创建纹理对象:
GLuint brickTexture = Utils::loadTexture("brick1.jpg");
纹理对象由整型ID标识,因此brickTexture的类型为GLuint。
#include <SOIL2/soil2.h>
GLuint Utils::loadTexture(const char* texImagePath) {
GLuint textureRef;// 存储创建的纹理对象(整型ID)
textureRef = SOIL_load_OGL_texture(texImagePath, SOIL_LOAD_AUTO, SOIL_CREATE_NEW_ID, SOIL_FLAG_INVERT_Y);
if (textureRef == 0) cout << "didnt find texture file " << texImagePath << endl;
// ----- 纹理映射/各向异性 section begin
glBindTexture(GL_TEXTURE_2D, textureRef);// 将纹理对象绑定到2D纹理
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);// 调整纹理设置(三线性过滤)
glGenerateMipmap(GL_TEXTURE_2D);// 为指定的纹理对象生成分级细化纹理
if (glewIsSupported("GL_EXT_texture_filter_anisotropic")) {// 判断GLEW扩展库是否支持纹理各向异性过滤
// 各向异性过滤参数值越大各向异性过滤的粒度就越大,如果值为1.0就代表普通的纹理过滤。
// 各向异性过滤会带来一定的开销。现代的显卡都已经支持各向异性过滤,而且做了优化。
GLfloat anisoset = 0.0f;
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &anisoset);// Get支持各向异性过滤的最大值
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, anisoset);// 设置各向异性过滤的最大值
}
// ----- 纹理映射/各向异性 section end
return textureRef;// 返回创建并设置好的纹理对象
}
————————————————————————————————————————————————————————————————————————————————————
unsigned int SOIL_load_OGL_texture(const char *filename, int force_channels,
unsigned int reuse_texture_ID, unsigned int flags);
从硬盘加载一张图片到OpenGL纹理。
[filename]:the name of the file to upload as a texture
[force_channels]:0(SOIL_LOAD_AUTO)-image format, 1-luminous, 2-luminous/alpha, 3-RGB, 4-RGBA
[reuse_texture_ID]:0(SOIL_CREATE_NEW_ID)-generate a new texture ID, otherwise reuse the texture ID (overwriting the old texture)
[flags]:can be any of SOIL_FLAG_POWER_OF_TWO | SOIL_FLAG_MIPMAPS | SOIL_FLAG_TEXTURE_REPEATS | SOIL_FLAG_MULTIPLY_ALPHA | SOIL_FLAG_INVERT_Y | SOIL_FLAG_COMPRESS_TO_DXT | SOIL_FLAG_DDS_LOAD_DIRECT
[return]:0-failed, otherwise returns the OpenGL texture handle
构建纹理坐标:
我们要定位纹理坐标,以便将砖纹理绘制到金字塔上。
相应的顶点和纹理坐标的一一对应关系如下图:
将纹理坐标载入缓冲区
先找出与金字塔模型的每个顶点一一对应的纹理坐标值。
每个三元顶点坐标vec3对应一个二元纹理坐标vec2。
金字塔有18个顶点,对应有18个(s, t)值,如下:
float pyrTexCoords[36] = {
0.0f,0.0f, 1.0f,0.0f, 0.5f,1.0f,// 前侧面
0.0f,0.0f, 1.0f,0.0f, 0.5f,1.0f,// 右侧面
0.0f,0.0f, 1.0f,0.0f, 0.5f,1.0f,// 后侧面
0.0f,0.0f, 1.0f,0.0f, 0.5f,1.0f,// 左侧面
0.0f,0.0f, 1.0f,1.0f, 0.0f,1.0f,// 底面三角形I
1.0f,1.0f, 0.0f,0.0f, 1.0f,0.0f // 底面三角形II
}
然后,在创建至少两个VBO(一个用于顶点vbo[0],一个用于纹理坐标vbo[1])之后,我们添加以下代码行以将纹理坐标加载到 VBO #2 中:
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glBufferData(GL_ARRAY_BUFFER, sizeof(pyrTexCoords), pyrTexCoords, GL_STATIC_DRAW);
在着色器中使用纹理:采样器变量和纹理单元
为了最大限度地提高性能,我们希望在硬件中执行纹理处理。这意味着我们的片段着色器需要一种访问我们在 C++/OpenGL 应用程序中创建的纹理对象的方法。它的实现机制是通过一个叫作统一采样器变量
的特殊 GLSL 工具。这是一个变量,用于指示[ 显卡 ]上的纹理单元
,从加载的纹理对象中提取或“采样”哪个纹素。
在片段着色器中声明一个统一采样器变量:
// 片段着色器
layout(binding = 0) uniform sampler2D samp;
声明的变量samp是sampler2D
类型的统一采样器变量,“binding=【0】”即指定此采样器与标记号为【0】的纹理单元相关联。
由于display()函数需要指定纹理单元为当前帧采样的纹理对象,所以每次绘制对象时,都需要激活纹理单元并将其绑定到特定的纹理对象。例如:
glActiveTexture(GL_TEXTURE0);// 其中“GL_TEXTURE”的后缀【0】与着色器中声明采样器变量中“binding=【0】”相对应
glBindTexture(GL_TEXTURE_2D, brickTexture);
glActiveTexture(GLenum texture)
texture-指定激活哪个纹理单元。
它必须是GL_TEXTUREi中的一个,其中i∈[0, GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS-1]
作用:选择后续纹理状态调用将影响的纹理单元。
void glBindTexture(GLenum target, GLuint texture)
target-指定纹理绑定到的目标。必须是下列之一:
GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D、
GL_TEXTURE_1D_ARRAY、GL_TEXTURE_2D_ARRAY、
GL_TEXTURE_RECTANGLE、
GL_TEXTURE_CUBE_MAP、GL_TEXTURE_CUBE_MAP_ARRAY、
GL_TEXTURE_BUFFER、
GL_TEXTURE_2D_MULTISAMPLE、GL_TEXTURE_2D_MULTISAMPLE_ARRAY
texture-指定纹理的名称。
作用:将命名纹理绑定到纹理目标。
要实际执行纹理处理,我们需要修改片段着色器输出颜色的方式。
请先理解
光栅化
,见Blog:✠OpenGL-2-图像管线,对光栅化主题有详尽的解释。
我们需要使用从顶点着色器(经过光栅着色器)接收的插值纹理坐标来对纹理对象进行采样,像这样调用 texture()
函数:
// first:顶点着色器
layout (location = 0) in vec3 pos;
layout (location = 1) in vec2 texCoord;
uniform mat4 xxxMatrix;
out vec2 tc;// 原样输出纹理坐标(在光栅着色器进行插值处理)
void main() {
gl_Position = xxxMatrix * vec4(pos, 1.0);
tc = texCoord;
}
// second:光栅着色器(不可编程)
这里完成的工作:将[纹理坐标]与[顶点坐标]一起进行插值处理,插值后的数据会被传递到下个可编程阶段——片段着色器。
// third:片段着色器
/* samp:统一采样器变量,它与标记号为【0】的纹理单元相关联;
在C++程序中创建的纹理对象,就是通过它来访问的。*/
layout(binding = 0) uniform sampler2D samp;
in vec2 tc;// 接收被插值后的纹理坐标
out vec4 color;
void main() {
// 用显卡的纹理单元,利用插值纹理坐标来对纹理对象(承载的是2D纹理图片数据)进行采样(提取纹素——像素颜色)
color = texture(samp, tc);
}
直观演示[纹理贴图]流程
下图,展示了:
纹理图片【纹理坐标:tex1(0,0)、tex2(1,0)、tex3(0.5,1)】
3D空间坐标系【顶点坐标:vert1(-1,0,0)、vert2(1,0,0)、vert3(0,3,0)】
纹理坐标texN 与 3D顶点坐标vertN,一一对应。
㊀C++/OpenGL程序:
将顶点坐标vert1、vert2、vert3 和 纹理坐标tex1、tex2、tex3 通过缓冲区传递进顶点着色器。
调用glDrawArrays(GL_TRIANGLES, 0, 3); 指示去绘制1个三角形(而不是其他形状)。
㊁顶点着色器:
layout(location=0) in vec3 vertCoord;// 接收顶点坐标数据
layout(location=1) in vec2 texCoord;// 接收纹理坐标数据
out vec2 tc;
main() {
【out】gl_Position = [变换矩阵] * vec4(vertCoord, 1.0);
【out】tc = texCoord;
}
㊂光栅化:
【in】gl_Position 转化为形成三角形的[像素]的位置坐标。
【in】tc 转化为形成三角形的[像素]的纹理坐标。
㊃片段着色器:
// 此采样器变量 samp 包含2D纹理图片的数据(从C++程序绑定而来),
// 所以从它上面根据像素位置来采样提取像素颜色。
layout(binding=0) uniform sampler2D samp;
in vec2 tc;
out vec4 color;
main() {
// 利用GPU纹理单元,根据[像素]纹理坐标tc,从2D纹理图片上采样。
// 输出的颜色是:从【纹理图片上相应[像素]位置】提取到的像素颜色值。
color = texture(samp, tc);
}
经纹理贴图后,三角形效果,如下图所示:(为了更直观,这里没考虑三角形矩阵变换等因素)
根据纹理的性质,有时可以通过改变纹理坐标分配方式来对抗纹理图片的压缩/拉伸情况,使得纹理需要较少的压缩/拉伸。
示例:金字塔模型纹理贴图
#include <SOIL2/soil2.h>
// 其他#include和以前一样
...
#define numVAOs 1
#define numVBOs 2
// 摄像机和对象位置、渲染程序、VAO和VBO的变量和以前一样
...
// display()函数的变量分配和以前一样
...
GLuint brickTexture;
void setupVertices() {
float pyramidPositions[54] = {/* 和以前一样 */}
float pyrTexCoords[36] = {
0.0f,0.0f, 1.0f,0.0f, 0.5f,1.0f,// 前侧面
0.0f,0.0f, 1.0f,0.0f, 0.5f,1.0f,// 右侧面
0.0f,0.0f, 1.0f,0.0f, 0.5f,1.0f,// 后侧面
0.0f,0.0f, 1.0f,0.0f, 0.5f,1.0f,// 左侧面
0.0f,0.0f, 1.0f,1.0f, 0.0f,1.0f,// 底面三角形I
1.0f,1.0f, 0.0f,0.0f, 1.0f,0.0f // 底面三角形II
}
// ... 和以前一样生成VAO和至少两个VBO,并加载两个缓冲区:
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(pyramidPositions), pyramidPositions, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glBufferData(GL_ARRAY_BUFFER, sizeof(pyrTexCoords), pyrTexCoords, GL_STATIC_DRAW);
}
void init(GLFWwindow* window) {
// 渲染程序配置、摄像机和对象位置没有改变
...
brickTexture = Utils::loadTexture("brick1.jpg");
}
void display(GLFWwindow* window, double currentTime) {
...
// 背景颜色、深度缓冲区、渲染程序,以及M、V、MV、PROJ矩阵没变化
...
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glVertexAttriPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glVertexAttriPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(1);
glActiveTexture(GL_TEXTURE0);// 激活【纹理单元】#0
glBindTexture(GL_TEXTURE_2D, brickTexture);// 绑定目标纹理
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glDrawArrays(TL_TRIANGLES, 0, 18);
}
int main() { /* 和以前一样 */ }
// 顶点着色器
#version 430
layout(location = 0) in vec3 pos;
layout(location = 1) in vec2 texCoord;
out vec2 tc;// 纹理坐标输出到【光栅着色器】用于插值
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
void main() {
gl_Position = proj_matrix * mv_matrix * vec4(pos, 1.0);
tc = texCoord;
}
// 片段着色器
#version 430
in vec2 tc;// 输入【插值过的】材质坐标
out vec4 color;
layout(binding = 0) uniform sampler2D samp;
void main() {
color = texture(samp, tc);
}
运行效果如下:
多级渐远纹理贴图 & 伪影
叠影伪影产生原理:
当图像分辨率小于所绘制区域的分辨率时,会出现一种很常见的伪影。在这种情况下,需要拉伸图像以覆盖整个区域,就会变得模糊(并且可能变形)。根据纹理的性质,有时可以通过改变纹理坐标分配方式来对抗这种情况,使得纹理需要较少的拉伸。另一种解决方案是使用更高分辨率的纹理图像。
相反的情况是当图像纹理的分辨率大于被绘制区域的分辨率时。可能并不是很容易理解为什么这会造成问题,但确实如此!在这种情况下,可能会出现明显的叠影伪影
,从而产生奇怪的错误图案,或移动物体中的“闪烁”效果。
叠影是由【采样错误】引起的。它通常与信号处理有关,不充分采样的信号被重建时,看起来会具有和实际不同的特性(例如波长)。如下图所示,原始波形显示为红色,沿波形的黄点代表采样点。如果采样点被用于重建波形,并且采样频率不足,则可能会定义出不同的波形(以蓝色显示)。
类似地,在纹理贴图中,当稀疏地采样高分辨率(和高细节)图像时(例如使用统一采样器变量时),提取到的颜色将不足以反映图像中的实际细节,而是可能看起来很随机。如果纹理图像具有重复图案,则叠影可能导致生成与原始图像不同的图案。如果被纹理贴图的对象正在移动,则纹素查找中的舍入误差可能导致给定纹理坐标处的采样像素的不断变化,从而在被绘制对象的表面上产生不希望的闪烁效果。
下图显示了一个立方体顶部的倾斜渲染特写, 该立方体使用大尺寸高分辨率棋盘图像进行纹理贴图。
在图像顶部附近明显发生了混叠,棋盘的欠采样产生了“条纹”效果。虽然我们无法在静止图像中展示,但如果这是一个动画场景,则看起来的图案可能会在各种不正确的图案(包括图示这个在内)之间波动。
另一个例子如下图所示,其中的立方体已经使用月球表面的图像进行纹理贴图。乍一看,这张图片显得清晰而细节丰富。然而,图像右上部分的某些细节是错误的,并且当立方体对象(或相机)移动时会导致“闪烁”。(不幸的是,我们无法在静止图像中清楚地显示闪烁效果。)
使用多级渐远纹理贴图来校正:
使用多级渐远纹理贴图(Mipmapping)
技术可以在很大程度上校正这一类的采样误差伪影,它需要用各种分辨率创建纹理图像的不同版本。然后, OpenGL 使用最适合正在处理的这一点处的分辨率的纹理图像进行纹理贴图。更好的是,可以为被贴图的区域使用最适合的分辨率的纹理图像的平均颜色。多级渐远纹理贴图应用于上面两图像的结果如下图所示。
多级渐远纹理贴图的校正原理:
多级渐远纹理贴图通过一种巧妙的机制来工作,它在纹理图像中存储相同图像的连续的一系列较低分辨率的副本,所用的纹理图像比原始图像大 1/3。这是通过将图像的 R、G 和B 分量分别存储在纹理图像空间的 3 个 1/4 中来实现的,然后在剩余的 1/4 图像空间中对于同一图像重复相当于原始分辨率 1/4 的处理。 重复该细分直到剩余象限太小而不包含任何有用的图像数据。
这种将几个图像填充到一个小空间中的方法(只比存储原始图像所需的空间大一点)是 Mipmapping 得名的原因。MIP 代表拉丁语Multum In Parvo,意思是“在很小的空间里有很多东西”。
多级渐远纹理对应的OpenGL选项:
实际给对象添加纹理时,可以通过多种方式对多级渐远纹理进行采样。在 OpenGL 中,可以通过将 GL_TEXTURE_MIN_FILTER
参数设置为所需的缩小方法来选择多级渐远纹理的采样方式,可以选取以下方法之一:
- GL_NEAREST_MIPMAP_NEAREST
选择具有与纹素区域最相似的分辨率的多级渐远纹理。然后,它获得所需纹理坐标的最近纹素。 - GL_LINEAR_MIPMAP_NEAREST
选择具有与纹素区域最相似的分辨率的多级渐远纹理。 然后它取最接近纹理坐标的4 个纹素的插值。这被称为“线性过滤
”。 - GL_NEAREST_MIPMAP_LINEAR
选择具有与纹素区域最相似的分辨率的 2 个多级渐远纹理。然后,它从每个多级渐远纹理获取纹理坐标的最近纹素并对其进行插值。这被称为“双线性过滤
”。 - GL_LINEAR_MIPMAP_LINEAR
选择具有与纹素区域最相似的分辨率的 2 个多级渐远纹理。然后,它取各自最接近纹理坐标的 4 个纹素,并计算插值。这被称为“三线性过滤
”。
【三线性过滤】通常是比较好的选择,因为较低的混合级别通常会产生伪影。
例如多级渐远纹理级别之间的可见分离。左下图显示了只启用了线性过滤的使用多级渐远纹理的棋盘的特写。请注意在多级渐远纹理的边界处垂直线突然从粗变为细(图中圈出的位置的伪影)。相比之下,右下图中的示例使用了三线性过滤。
OpenGL 提供了丰富的多级渐远纹理支持。有一些机制可用于构建你自己的多级渐远纹理级别,或者让 OpenGL 为你构建它们。
在大多数情况下,OpenGL自动构建的多级渐远纹理已足够。这是通过将以下代码行添加进 getTextureObject()函数之后立即执行的 Utils::loadTexture()函数中实现的:
GLuint getTextureObject(GLuint textureID) {
glBindTexture(GL_TEXTURE_2D, textureID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glGenerateMipmap(GL_TEXTURE_2D);
return textureID;
}
这通知 OpenGL 生成多级渐远纹理。使用 glBindTexture()调用激活砖纹理,然后glTexParameteri() 函数调用启用前面列出的缩小方法之一 ,例如上面调用中显示的GL_LINEAR_MIPMAP_LINEAR,它启用三线性过滤。
构建多级渐远纹理后,可以通过再次调用 glTexParameteri()来更改过滤选项(尽管这很少需要),例如在 display 函数中。甚至可以通过选择 GL_NEAREST 或GL_LINEAR 来禁用多级渐远纹理。
对于关键应用程序,可以使用你喜欢的任何图像编辑软件自行构建多级渐远纹理。然后可以通过为每个多级渐远纹理级别重复调用 OpenGL 的 glTexImage2D()
函数来创建纹理对象,并将它们添加为多级渐远纹理级别。这里不做过多讨论。
各向异性过滤
多级渐远纹理贴图有时看起来比非多级渐远纹理贴图更模糊(下图比较可知),尤其是当被贴图对象以严重倾斜的视角渲染时。我们在左下图中看到了一个这样的例子,右下图使用多级渐远纹理减少伪影的同时也减少了图像细节。
这种细节的丢失是因为当物体倾斜时,其基元看起来沿一个轴(即宽度或高度)比沿另一个轴更小。当 OpenGL 为图元贴图时,它选择适合两个轴中较小的轴的多级渐远纹理(以避免“闪烁”伪影)。在右上图中,表面远离观察者倾斜,因此每个渲染图元将使用适合其更小的高度的多级渐远纹理,这可能对其宽度来说分辨率太小了。
一种恢复一些丢失细节的方法是使用各向异性过滤(AF)
。标准的多级渐远纹理贴图以各种正方形分辨率(例如 256 像素× 256 像素、 128 像素× 128 像素等)对纹理图像进行采样,而 AF 却以多种矩形分辨率对纹理进行采样,例如 256 像素× 128 像素、 64 像素× 128像素等。这使得从各种角度观看并同时在纹理中保留尽可能多的细节成为可能。
各向异性过滤比标准多级渐远纹理贴图在计算上代价更高, 并且不是 OpenGL 的必需部分。但是,大多数显卡都支持 AF(这被称为 OpenGL 扩展),而 OpenGL 确实提供了一种查询显卡是否支持 AF 的方法,以及一种访问 AF 的方法。生成多级渐远纹理贴图后立即添加代码:
// 如果启用【多级渐远纹理贴图】
glBindTexture(GL_TEXTURE_2D, textureID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glGenerateMipmap(GL_TEXTURE_2D);
// 如果还启用【各向异性过滤】
if (glewIsSupported("GL_EXT_texture_filter_anisotropic")) {
GLfloat anisoSetting = 0.0f;
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &anisoSetting);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, anisoSetting);
}
对glewIsSupported()
的调用测试显卡是否支持 AF。如果支持,我们将其设置为支持的最大采样程度,这个最大值使用glGetFloatv()获取。然后使用 glTexParameterf()将其应用于激活纹理对象。
启用【多级渐远纹理贴图】,并启用【各向异性过滤】,效果如下:
从上图并对比之上的两图,可以看到,大部分丢失细节已经恢复,同时仍然消除了闪烁的伪影。
这时,我们的loadTexture函数,可以改成:
GLuint Utils::loadTexture(const char* texImagePath) {
GLuint textureRef;
textureRef = SOIL_load_OGL_texture(texImagePath, SOIL_LOAD_AUTO, SOIL_CREATE_NEW_ID, SOIL_FLAG_INVERT_Y);
if (textureRef == 0)
cout << "didnt find texture file " << texImagePath << endl;
// ----- mipmap/anisotropic section
glBindTexture(GL_TEXTURE_2D, textureRef);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);// 三线性过滤
glGenerateMipmap(GL_TEXTURE_2D);
if (glewIsSupported("GL_EXT_texture_filter_anisotropic")) {
GLfloat anisoset = 0.0f;
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &anisoset);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, anisoset);// 各种异性过滤
}
// ----- end of mipmap/anisotropic section
return textureRef;
}
环绕和平铺
到目前为止,我们假设纹理坐标都落在[0…1]范围内。但是, OpenGL 实际上支持任何取值的纹理坐标。有几个选项可以用来指定当纹理坐标超出范围[0…1]时会发生什么。使用glTexParameteri()
设置所需的行为,选项如下。
- GL_REPEAT:忽略纹理坐标的整数部分,生成重复或“平铺”图案。这是默认行为。
- GL_MIRRORED_REPEAT:忽略整数部分,但是当整数部分为奇数时坐标反转,因此重复的图案在正常和镜像之间交替。
- GL_CLAMP_TO_EDGE:小于 0 或大于 1 的坐标分别设置为 0 和 1。
- GL_CLAMP_TO_BORDER:将[0…1]以外的纹素设置成指定的边框颜色。
例如,考虑一个金字塔,其纹理坐标已在[0…5]范围,而不是通常的[0…1]范围内定义。默认行为(GL_REPEAT),使用前面图中显示的纹理图像,将导致纹理在表面上重复五次(有时称为“平铺”),如下图所示。
GL_MIRRORED_REPEAT:使平铺块的外观在正常和镜像之间交替。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
GL_CLAMP_TO_EDGE:将指定 < 0 或 > 1 的值分别设置为0和1。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
GL_CLAMP_TO_BORDER:指定 < 0 或 > 1的值输出“边框”颜色。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
float redColor[4] = { 1.0f, 0.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, redColor);
纹理坐标范围为−2~+3,效果如下(从左往右分别是:镜像重复、夹紧到边缘、夹紧到边框):
-2 ~ +3的纹理坐标范围,使得镜像重复示例,最底下是重复五次平铺。
中间的示例(夹紧到边缘)中,沿纹理图像边缘的像素向外复制。
透视变形
考虑一个由两个三角形组成的矩形,纹理贴图是棋盘图像,面向相机。当矩形围绕 X轴旋转时,矩形的顶部会倾斜并远离相机,而矩形的下半部分则更靠近相机。因此,我们希望顶部的方块变小,底部的方块变大。但是,纹理坐标的线性插值将导致所有正方形的高度相等。沿着构成矩形的两个三角形之间的对角线的失真加剧。产生的失真如图:
幸运的是,存在用于校正透视失真的算法,并且默认情况下, OpenGL 在光栅化期间会应用透视校正算法
。下图显示了由 OpenGL 正确呈现的相同的旋转棋盘。
可以禁用OpenGL的透视校正,虽然不常见。
必须在顶点着色器和片段着色器中都添加关键字“noperspective
”。
// 顶点着色器
noperspective out vec2 texCoord;
// 片段着色器
noperspective out vec2 texCoord;
用C++/OpenGL实现纹理贴图(不利用SOIL2库)
可以使用 C++和 OpenGL 函数直接将纹理图像文件数据加载到 OpenGL 中。 虽然它有点复杂,但并不少见。一般步骤如下:
(1)使用C++工具读取图像文件;
(2)生成OpenGL纹理对象;
(3)将图像文件数据复制到纹理对象中。
步骤(1):可以使用C++函数fopen()和fread()就可以将数据从.bmp图像文件读入unsigned char类型的数组中。
步骤(2):使用OpenGL的glGenTextures()
命令创建一个或多个纹理对象。例如,生成单个OpenGL纹理对象:
GLuint textureID;// 或者GLuint类型的数组,如需创建多于一个纹理对象
glGenTextures(1, &textureID);
——————————————————————————————————————————————
void glGenTextures(GLsizei n, GLuint* textures)
n-指定要生成的纹理名称的数目。
textures-指定一个数组去存储生成的纹理名称。
步骤(3):将步骤(1)中的图像数据关联到步骤(2)中创建的纹理对象,这里使用glTexImage2D()
命令完成——从步骤(1)中描述的 unsigned
char 数组(此处表示为“data”)加载到步骤(2)中创建的纹理对象中:
glBindTexture(GL_TEXTURE_2D, textureID)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_BGR, GL_UNSIGNED_BYTE, data);
——————————————————————————————————————————————
void glTexImage2D(
GLenum target, 指定目标纹理。
GLint level, 指定详细级别编号。级别0是基本图像级别。第n级是第n幅mipmap约简图像。
GLint internalformat, 指定纹理中颜色组件的数量。如GL_RED、GL_R16、GL_COMPRESSED_RGBA等等。
GLsizei width, 指定纹理图像的宽度。所有的实现都支持至少1024纹素宽的纹理图像。
GLsizei height, 指定纹理图像的高度,或纹理数组中的层数等。
GLint border, 这个参数必须是0。
GLenum format, 指定像素数据的格式。
GLenum type, 指定像素数据的数据类型。
const void * data 指定一个指向内存中的图像数据的指针。
);
作用:指定二维纹理图像。将图像文件(data)数据复制到纹理对象中。如果没有指定图像文件,则data参数设置为0。
前面介绍的用于设置多级渐远纹理贴图等的各种glTexParameteri()调用也可以应用于纹理对象。
生成多个纹理的伪例子
一个六个面的多纹理贴图立方体示例(伪代码):
GLuint texture[6]; // Storage For Six textures
................
addGLTextures() {
AUX_RGBImageRec *TextureImage[6]; // 创建纹理的存储空间
TextureImage[0]=LoadBMP("MM/01.bmp");
TextureImage[1]=LoadBMP("MM/02.bmp");
TextureImage[2]=LoadBMP("MM/03.bmp");
TextureImage[3]=LoadBMP("MM/04.bmp");
TextureImage[4]=LoadBMP("MM/05.bmp");
TextureImage[5]=LoadBMP("MM/06.bmp");
glGenTextures(6, &texture); // 创建6个纹理对象
for (int i = 0; i < 6; i++) {
glBindTexture(GL_TEXTURE_2D, texture[i]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB,
TextureImage[i]->sizeX, TextureImage[i]->sizeY,
0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[i]->data);
//...设置多级渐远纹理贴图等的各种glTexParameteri()调用...
}
}
注:如果需要创建多于一个纹理对象,也可以用二维纹理数组,让参数target=GL_TEXTURE_2D_ARRAY。
补充说明
研究人员开发了纹理单元的许多用途,不仅仅是场景中的纹理模型。在后面的章节中,我们将看到如何使用纹理单元来改变物体反射光线的方式,使其看起来凹凸不平。我们还可以使用纹理单元来存储“高度图”以生成地形,以及存储“阴影贴图”以有效地为场景添加阴影。这些用途将在后续章节中描述。
着色器还可以向纹理写入数据,允许着色器修改纹理图像,甚至将一个纹理的一部分复制到另一个纹理的某个部分。
多级渐远纹理贴图和各向异性过滤不是减少纹理中的叠影伪影的唯一工具。
例如, 全屏抗锯齿(Full-scene anti-aliasing, FSAA)
和其他超采样方法也可以改善 3D 场景中纹理的外观。
虽然它们不是OpenGL核心的一部分,但它们通过OpenGL 的扩展机制在许多显卡上得到支持。
还有一种用于配置和管理纹理和采样器的替代机制。 OpenGL 3.3 版引入了采样器对象
(有时称为“采样器状态”——不要与采样器变量混淆),可用于保存一组独立于实际纹理对象的纹理设置。【采样器对象】附加到纹理单元,可以方便有效地更改纹理设置。
这里暂不介绍【采样器对象】。对于感兴趣的读者,采样器对象的使用很容易学习,并且有许多优秀的在线教程。
附:官方SOIL2库使用样例
https://github.com/SpartanJ/soil2
关于函数参数的解释,请在IDE中查看源码,源码有对参数作详细解释。
#include <SOIL2/soil2.h>
// 加载一张图片文件,直接作为一个新的OpenGL纹理
GLuint tex_2d = SOIL_load_OGL_texture
(
"img.png",
SOIL_LOAD_AUTO,
SOIL_CREATE_NEW_ID,
SOIL_FLAG_MIPMAPS | SOIL_FLAG_INVERT_Y | SOIL_FLAG_NTSC_SAFE_RGB | SOIL_FLAG_COMPRESS_TO_DXT
);
// 检查加载过程中是否出现错误(这里是检查上面代码加载过程的状态)
if(0 == tex_2d) {
printf("SOIL loading error: '%s'\n", SOIL_last_result());
}
// 加载另一张图片,但加载到同一纹理ID,覆盖先前的纹理对象(上面代码创建的纹理ID)
tex_2d = SOIL_load_OGL_texture
(
"some_other_img.dds",
SOIL_LOAD_AUTO,
tex_2d,
SOIL_FLAG_DDS_LOAD_DIRECT
);
// 加载6张图片到一个新的OpenGL立方体贴图,并强制使用RGB颜色格式
GLuint tex_cube = SOIL_load_OGL_cubemap
(
"xp.jpg", "xn.jpg", "yp.jpg", "yn.jpg", "zp.jpg", "zn.jpg",
SOIL_LOAD_RGB,
SOIL_CREATE_NEW_ID,
SOIL_FLAG_MIPMAPS
);
// 用默认格式,加载并分割单张图片形成一个新的OpenGL立方体贴图
// 朝向顺序:East South West North Up Down => "ESWNUD",大小写是敏感的!
GLuint single_tex_cube = SOIL_load_OGL_single_cubemap
(
"split_cubemap.png",
"EWUDNS",
SOIL_LOAD_AUTO,
SOIL_CREATE_NEW_ID,
SOIL_FLAG_MIPMAPS
);
// 用默认格式,在OpenGL立方体贴图上加载DDS立方体贴图(这里的OpenGL立方体贴图是正上面代码创建的对象)
// 可尝试直接加载,但给出面的朝向顺序以防失败
// DDS立方体贴图的面朝向顺序是预先定义好的——SOIL_DDS_CUBEMAP_FACE_ORDER
single_tex_cube = SOIL_load_OGL_single_cubemap
(
"overwrite_cubemap.dds",
SOIL_DDS_CUBEMAP_FACE_ORDER,
SOIL_LOAD_AUTO,
single_tex_cube,
SOIL_FLAG_MIPMAPS | SOIL_FLAG_DDS_LOAD_DIRECT
);
// 加载一张图片作为高度图,强制为灰度图(因此通道channels应为1)
int width, height, channels;
unsigned char *ht_map = SOIL_load_image
(
"terrain.tga",
&width, &height, &channels,
SOIL_LOAD_L
);
// 将图片保存为另一种格式(这里图片是上面代码创建的高度图)
int save_result = SOIL_save_image
(
"new_terrain.dds",
SOIL_SAVE_TYPE_DDS,
width, height, channels,
ht_map
);
// 保存一张OpenGL游戏引擎的屏幕截图,分辨率为1024x768
save_result = SOIL_save_screenshot
(
"awesomenessity.bmp",
SOIL_SAVE_TYPE_BMP,
0, 0, 1024, 768
);
// (PhysicsFS库对各种压缩文件的访问提供了一个统一的抽象接口。它用于视频游戏。)
// 通过PhysicsFS加载一张图片,需要从内存(RAM)中解压这张图片文件,
// 它在一个缓冲区:unsigned char* image_in_RAM
GLuint tex_2d_from_RAM = SOIL_load_OGL_texture_from_memory
(
image_in_RAM,
image_in_RAM_bytes,
SOIL_LOAD_AUTO,
SOIL_CREATE_NEW_ID,
SOIL_FLAG_MIPMAPS | SOIL_FLAG_INVERT_Y | SOIL_FLAG_COMPRESS_TO_DXT
);
// 释放占用的内存(这里释放的是上面代码创建的高度图对象)
SOIL_free_image_data(ht_map);