OpenGL:纹理贴图

纹理贴图是在栅格化的模型表面上覆盖图像的技术。它是为渲染场景添加真实感的最基本和最重要的方法之一。硬件也为纹理贴图提供了硬件支持,使得它具备实现实时的照片级真实感的超高性能。纹理单元是专为纹理设计的硬件组件,现代显卡通常带有数个纹理单元。

为了在OpenGL/GLSL中有效完成纹理贴图,需要协调好以下几个不同的数据集和机制:

  1. 用于保存纹理图像的纹理对象

  1. 特殊的统一采样器变量,以便顶点着色器访问纹理

  1. 用于保存顶点纹理坐标的缓冲区

  1. 用于将顶点纹理坐标传递给管线的顶点属性

  1. 显卡上的纹理单元

一、如何将纹理图像加载到OpenGL

纹理图像可以是任何图像。在电子游戏和动画电影中,纹理图像通常用于为角色或者生物绘制皮肤和衣服。图像通常存在如.jpg、.png、.gif、.tiff等图像文件中。为了使纹理图像应用于OpenGL管线中的着色器,我们需要从图像中提取颜色并将它们放入OpenGL纹理对象(用于保存纹理图像的内置OpenGL结构)中。

通常将纹理加载到OpenGL应用程序的步骤是:

setp1:创建纹理对象

许多C++库都可以读取和处理图像文件,比如SOIL2。使用SOIL2实例化OpenGL纹理对象并从图像文件中读入数据操作如下。

/*创建一个纹理对象,纹理对象由整型ID标识,因此首先要声明一个GLunit类型的变量来保存它。
  这里需要注意OpenGL纹理对象所采用的方向左下角的坐标为(0,0),右上角的坐标为(1,1), 
这与存储在许多标准图像文件格式中的图像的方向不同,在那些图像中原点位于左上角;此时可以
通过指定SOIL_FLAG_INVERT_Y参数垂直翻转图像来重新定向使其与OpenGL的预期格式相对应)*/
GLunit textureID = SOIL_load_OGL_texture("image.jpg", SOIL_LOAD_AUTO, 
    SOIL_CREATE_NEW_ID, SOIL_FLAG_INVERT_Y)

当然也可以不使用第三方库,一般步骤为:

先使用C++工具读取图像文件数据(比如fopen和fread函数将数据从.bmp图像文件读入到unsigned char类型的数组,比如这里为数组data);

接着生成OpenGL纹理对象,可以使用如下OpenGL命令来创建一个或多个纹理对象。

//创建一个纹理对象。如果要创建多个,则使用GLuint类型的数组。
GLuint textureID;
glGenTextures(1, &textureID);

最后将图像文件数据复制到创建的纹理对象中,可以使用如下命令来完成:

glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0 , 
                GL_BGR, GL_UNSIGNED_BYTE, data)

setp2:调用如下函数以使新创建的纹理对象处于激活状态。

这里需要注意显卡的纹理单元(和相关的采样器),可以对任何我们希望的纹理对象进行采样,也可以在允许时更改。在displasy()时需要指定纹理单元要为当前帧采样的纹理对象。因此在每次绘制对象时都要激活纹理单元并将其绑定到特定纹理对象。例如:

/*指定GL_TEXTURE0,使得第0个纹理单元处于激活状态(纹理单元的数量取决于显卡提供的数量)*/
glActiveTexture(GL_TEXTURE0);
/*其第2个参数就是如上的textureID*/
glBindTexture(GL_TEXTURE_2D, textureID);

setp3:调整纹理设置,比如:

多级渐远纹理贴图(mipmapping)

当纹理图像的分辨率小于所绘制区域的分辨率时,会出现一种很常见的伪影。相反图像纹理的的分辨率大于被绘制区域的分辨率的话可能会出现叠影(叠影是由采样误差引起的),使用多级渐远纹理贴图技术可以很大程度来校正这一采用误差。OpenGL提供了丰富的多级渐远纹理支持。其中一些机制可以让OpenGL自动构建(如下代码用于通知OpenGL生成多级渐远纹理),大多数情况下,OpenGL自动构建多级渐远纹理已足够。

...
//1)如果使用多级渐远纹理贴图则执行如下几行代码
/*其第2个参数就是如上的textureID*/
glBindTexture(GL_TEXTURE_2D, textureID);
/*实际给对象添加纹理时,可以通过多种方法对多级渐远进行采样。在OpenGL中可以通过将
GL_TEXTURE_MIN_FILTER参数设置为所需的缩小方法来选择多级渐远的采用方法,而
GL_LINEAR_MIPMAP_LINEAR便是可选取的方法之一,它表示选择具有与纹理区域最相似的分辨率的2个
多级渐远纹理。然后,它取各自最接近纹理坐标的4个texel,并计算插值,即三线性过滤。*/
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glGenerateMipmap(GL_TEXTURE_2D);
//2)如果还使用各向异常型
......

另一些机制可用于构建属于自己的多级渐远纹理级别。对于特殊的场景,可以使用任何一款图像编辑软件自行构建多级渐远纹理,然后通过为每个多级渐远纹理级别重复调用OpenGL的glTexImage2D()函数来创建纹理对象,并将它们添加为多级渐远纹理级别。

2)各向异性过滤(Anisotropic Filtering, AF)

多级渐远纹理贴图有时候看起来比非多级渐远纹理贴图更加模糊,尤其是当被贴图对象以严重倾斜的视角渲染时,可见使用多级渐远纹理贴图在减少伪影的同时也损失了图像细节(这种细节的丢失是因为当物体倾斜时,其图元看起来在一个轴(即沿宽或高)上的尺寸比在另一个轴上更小)。

一种恢复一些丢失细节的方法是使用各向异性过滤技术;标准的多级渐远纹理贴图以各种正方形分辨率(如256像素*256像素)对纹理图像进行采样,而各向异性过滤却以多种矩形分辨率对纹理进行采样。这使得从各种角度观看的纹理都保留尽可能多的细节。

大多数显卡都支持各向异性过滤(成为OpenGL扩展),OpenGL也提供了查询和访问各向异性的方法,只是需要在多级渐远纹理贴图处理后立即添加代码:

...
//1)如果使用多级渐远纹理贴图
......
//2)如果还使用各向异常型则接着执行如下代码
/*glewIsSupported用来测试显卡是否支持各向异性过滤*/
if (glewIsSupported("GL_EXT_texture_min_filter_anisotropic")) {
    GLfloat anisoSetting = 0.0f;
    /*如果支持各向异性过滤,将其设置为支持的最大采样程度,这个值通过glGetFloatv获取。并
    调用glTexParameterf将其应用于激活纹理对象。*/
    glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &anisoSetting);
    glTexParameterf(GL_TEXTURE_2D, GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, anisoSetting);
}

二、如何构建顶点的纹理坐标并将其载入到缓冲区

1)构建顶点的纹理坐标

有了上述的将纹理图像加载的OpenGL的方法,还需要指定希望如何将纹理应用于对象的渲染表面。可以通过为模型中的每个顶点指定顶点纹理坐标来完成此操作。

顶点纹理坐标是对纹理图像中像素的引用,用于将3D模型上的点映射到纹理中的位置。纹理图像中的像素被称为texel(纹元),以便将它们与在屏幕上呈现的像素区分开。模型表面上的每个点除了将它定位在3D空间中的坐标(x,y,z)外,还具有顶点纹理坐标,比如最为常见的2D纹理坐标(s,t)用来指定纹理图像中的哪个texel为它提供颜色;这样物体的表面被按照纹理图像"涂画"。

要使用纹理贴图,必须为要添加纹理的对象中的每个顶点提供顶点纹理坐标。OpenGL将使用这些顶点纹理坐标,查找存储在纹理图像中的引用的texel的颜色,来确定模型中每个栅格化像素的颜色。为了确保渲染模型中的每个像素都使用纹理图像中的适当texel进行绘制,顶点纹理坐标也需要放入顶点属性中,以便由光栅着色器进行插值。以这种方式,纹理图像与模型顶点一起被插值或者填充。

对于通过顶点着色器的每组顶点空间坐标(x,y,z),会有一组相应的顶点纹理坐标(s,t),因此需要设置二个缓冲区,一个用于顶点的空间坐标,另一个用于相应的纹理坐标。这样,每次顶点着色器的调用会接收到一个顶点的数据(包括其空间坐标和相应的纹理坐标)。

在2D纹理坐标中,2D纹理图像被设定为矩形,左下角的位置为(0,0),右上角的坐标位置为(1,1);理想情况下纹理坐标应该在[0,1]区间内取值。

/*比如一个立方体的纹理坐标如下所示。这里需要注意的是我们这里是假定落在[0, 1]之间,但是
但是OpenGL实际上支持任何取值范围的纹理坐标,可以通过glTextParameteri()来设置*/
float textureCoordinates[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,
        1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f
    };

这里需要注意对于立方体等这样的简单模型,选择纹理坐标比较容易。但是对于具有大量三角形的更复杂的弯曲模型,手动确定它们的纹理坐标是不切实际的。在弯曲的几何形状(例如球面或环面)的情况下,可以通过算法或数学方式计算纹理坐标。使用Maya或者Blender等建模工具构建模型时可以使用"UV映射功能"使得纹理坐标的任务更容易完成。

2)将顶点的纹理坐标载入缓冲区

和顶点的空间坐标类似,将纹理坐标加载到另一个VBO中。比如将纹理坐标放入vbo1中。其中textureCoordinates保存的是纹理坐标。

 glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
 glBufferData(GL_ARRAY_BUFFER, sizeof(textureCoordinates), textureCoordinates, GL_STATIC_DRAW);

三、如何在着色器中使用纹理:采样器变量和纹理单元

为了提高性能,可以在显卡硬件中执行纹理处理。这就意味着片着色器需要一种访问我们在C++/OpenGL应用程序中创建的纹理对象的方法。

可以通过在着色器中声明“统一采样器变量”来指示显卡上的纹理单元,从加载的纹理对象中提取或采样texel,如下所示:

/*其中的layout (binding=0)部分用于指定此采样器与第0个纹理单元关联;samp便是声明的变量*/
layout (binding=0) uniform sampler2D samp;

要实际执行纹理处理,还需要注意片段着色器的输出颜色的方式,需要使用从顶点着色器(通过光栅着色器)接收的插值纹理坐标来对纹理对象进行采样。调用texure函数如下:

in vec2 tc; //纹理坐标
...
color = texture(samp, tc);

1)顶点着色器示例

#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;
layout (binding=0) uniform sampler2D samp;  //顶点,着色器中未使用

void main(void)
{    gl_Position = proj_matrix * mv_matrix * vec4(pos,1.0);
    tc = texCoord;
} 

2)片段着色器示例

#version 430

in vec2 tc;  //输入插值过的纹理坐标
out vec4 color;

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
layout (binding=0) uniform sampler2D samp;

void main(void)
{    color = texture(samp, tc);
}

你可能感兴趣的:(OpenGL,图形渲染)