OpenGL学习——纹理

之前的文章我们理解了OpenGL运行的基本原理和绘制过程。我们可以通过编写着色器脚本来实现每一个像素点的绘制操作,但是我们如何实现图片的渲染呢(总不能图片也要我们一个点一个点操作吧)。这里通过引入纹理这个概念来实现图片的渲染。

所以,什么是纹理呢?

纹理基本概念

材质贴图,又称纹理贴图,在计算机图形学中是把存储在内存里的位图包裹到3D渲染物体的表面。纹理贴图给物体提供了丰富的细节,用简单的方式模拟出了复杂的外观。一张图像(纹理)被贴(映射)到场景中的一个简单形体上,就像印花贴到一个平面上一样。这大大减少了在场景中制作形体和纹理的计算量。

这是Wiki上的说法,简单的说纹理就是一副只读图像,而把一副图像映射到图形上的过程叫做纹理映射。从另一个角度来说它是一个只读数据容器,而通常它用于存储图像信息。

纹理图像数据存在多种色彩格式(Pixelformat)和多种的存储格式(PixelType)

这两个属性共同决定了纹理中的每一个数据单元–纹素(Texel)的构成。所以值得注意的是当我们加载一张纹理的时候必须要注意图像的输入输出格式是否和OpenGL中一一对应。

纹理坐标

当渲染纹理时,纹理的坐标将成为索引。GL_TEXTURE_2D纹理的坐标轴分别是(s,t)或者(u,v)。

2D纹理的坐标系是从左下角为原点向上为t轴,向右为s轴的坐标系。这个坐标系的y方向和GL坐标系相反,所以默认按顶点坐标系方向输入的图像是倒置的。

如果想要让纹理正向显示,我们需要做的一步工作便是将输入的纹理坐标或者顶点坐标进行y轴方向的倒置。

纹理(处理)单元

这里存在两个概念:

  • 纹理目标(一张纹理图像)
  • 纹理单元(纹理图像的处理单元)

形如GL_TEXTURE0GL_TEXTURE1GL_TEXTURE2的就是纹理单元,一台机子上纹理单元数量是有限的,依具体机型而定。通过glActiveTexture来激活具体的纹理单元。

一个纹理单元又包含多个类型的纹理目标包括:GL_TEXTURE_1DGL_TEXTURE_2DGL_TEXTURE_3DCUBE_MAP等等。其中GL_TEXTURE_2D类型的纹理是我们最常见的处理对象。

1
2
3
4
// 激活纹理单元0并绑定一个2D纹理,最后将单元0句柄传入着色器
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, self.texture.texture);
glUniform1i(self.textureUniform, 0);

纹理环绕

纹理坐标的范围通常是从[0, 0]到[1, 1],超过[0.0, 1.0]的范围是允许的,而对与超出范围的内容要如何显示,这就取决于纹理的环绕方式(Wrapping mode)。在OpenGL默认的行为是重复这个纹理图像(GL_REPEAT)。

环绕方式(Wrapping) 描述
GL_REPEAT 对纹理的默认行为。重复纹理图像。
GL_MIRRORED_REPEAT 和GL_REPEAT一样,但每次重复图片是镜像放置的。
GL_CLAMP_TO_EDGE 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。

下图从左到右依次是这三种效果:
GL_CLAMP_TO_EDGE,GL_REPEAT,GL_MIRRORED_REPEAT

同时需要注意的是,在iOS中GL_REPEAT,GL_MIRRORED_REPEAT这两种环绕模式是不支持长或宽非2^n 大小的图(会渲染失败完全黑掉)。

我们通过glTexParameteri指定Texture的环绕模式,可以看见它是可以在S,T两个方向上独立设置的。

1
2
3
// 设置Texutre的环绕模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

假如我们在不同方向上设置不同的环绕模式,便会有不同的效果。

1
2
3
// 设置Texutre的环绕模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);

纹理滤波

纹理坐标不依赖于分辨率(Resolution),它可以是任意浮点值,所以OpenGL需要知道怎样将纹理像素(Texture Pixel–Texel)映射到纹理坐标。

当你有一个很大的物体但是纹理的分辨率很低的时候这就变得很重要了(如何处理低分辨率的像素锯齿)。OpenGL对于上述情况会进行纹理过滤(Texture Filtering),在ES中支持GL_NEAREST和GL_LINEAR,这两种滤波方式依据不同的算法会得出不同的像素结果。

  • GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。

  • GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。

下图从左到右依次是这两种效果:
GL_NEAREST,GL_LINEAR

可以很直观的看出来,这两种滤波方式的不同。同时这两个参数也可以从两个方向设置,当进行放大(Magnify)和缩小(Minify)操作的时候可以设置纹理过滤的方式。如下,放大纹理和缩小纹理的时候都进行线性滤波。

1
2
3
// 设置Texutre的滤波方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

多级渐远纹理(Mipmap)

假如我们将一张图缩小非常多倍的时候这时候,由于远处的物体可能只产生很少的片段,OpenGL从高分辨率纹理中为这些片段获取正确的颜色值就很困难,因为它需要对一个跨过纹理很大部分的片段只拾取一个纹理颜色。在小物体上这会产生不真实的感觉,更不用说对它们使用高分辨率纹理浪费内存的问题了。

OpenGL使用一种叫做多级渐远纹理(Mipmap)的概念来解决这个问题,它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。

由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好。

可以看到多级渐远纹理(Mipmap)是建立在缩小(Minify)纹理这种情况下的。

1
2
3
4
5
6
7
// 纹理单元开启MipMap
glTexParameterf(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
glGenerateMipmap(GL_TEXTURE_2D);

// 设置Texutre的滤波方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

总结

Demo工程:OpenGL学习实例
这篇文章介绍了OpenGL中纹理的基本概念和它的一些基本参数。