一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(理论篇)

3,210 阅读12分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

通过阅读本文,你将获得以下收获:
1.什么是纹理
2.纹理如何进行映射
3.纹理映射相关细节要点

上篇回顾

上一篇一看就懂的OpenGL ES教程——缓冲对象优化程序(二)已经讲完了OpenGL中三种最常见的缓冲对象如何优化程序,但是前面博文讲的图元着色显得过于单一,能不能绘制像一幅画一样具有丰富的图像呢?当然可以。有了前面知识的铺垫,今天就可以开始接触OpenGL一个非常有趣重要的知识点——纹理映射。通过纹理映射,我们可以实现体验临摹画手的浪漫之旅了。

我可以预感这又是生动有趣的一堂课。

126623f555d58c5a033546c6e81293f0.jpeg

由于纹理映射内容比较多,所以将理论和实践拆分为2篇博文,本文就是先从理论知识入手讲解。

纹理对象

首先什么是纹理呢?看到这个名字,很多人第一反应很可能是以下的条纹状图:

fa531f9b8b620efb287806c2eeda4cab.jpeg

在OpenGl中,纹理的概念和这个还是有区别的,用通俗的说法,其实纹理就是图片,或者严格来说,就是存储图片数据的容器,马上来个官方定义(纹理 ) :

texture is an OpenGL Object that contains one or more images that all have the same image format. A texture can be used in two ways: it can be the source of a texture access from a Shader, or it can be used as a render target.

一个纹理texture,就是一个OpenGL Object(如果你很认真看过一看就懂的OpenGL ES教程——缓冲对象优化程序(一)并且记忆力很好,也许记得在里面有闪现过texture的字样),这是一个包含一张或者多张格式相同的图片信息的OpenGL Object。它可以用来被shader访问,也可以被直接绘制

通俗来说,纹理就是包含着一张或者多张图片信息的一个OpenGL对象,这张图片可以是一维、二维或者三维的,并且拥有对应的图片格式

纹理保存着纹理类型、纹理大小,图片类型。其中纹理类型很多,有:

  • GL_TEXTURE_1D: Images in this texture all are 1-dimensional. They have width, but no height or depth.
  • GL_TEXTURE_2D: Images in this texture all are 2-dimensional. They have width and height, but no depth.
  • GL_TEXTURE_3D: Images in this texture all are 3-dimensional. They have width, height, and depth.
  • GL_TEXTURE_RECTANGLE: The image in this texture (only one image. No mipmapping) is 2-dimensional. Texture coordinates used for these textures are not normalized.
  • GL_TEXTURE_BUFFER: The image in this texture (only one image. No mipmapping) is 1-dimensional. The storage for this data comes from a Buffer Object.
  • GL_TEXTURE_CUBE_MAP: There are exactly 6 distinct sets of 2D images, each image being of the same size and must be of a square size. These images act as 6 faces of a cube.
    ……

等等很多种,本文仅针对GL_TEXTURE_2D进行讲解,因为这是我们最熟悉的2维纹理,即平面图片

纹理映射

纹理映射咋一看还是很让人产生不明觉厉的感觉的,不过用一个通俗的叫法表达,大家就觉得亲切多了:贴图

如下图所示,将2张图贴到一个物体表面:

6cabab92c446d426b1e0512ea3049d5a.jpeg

所以学习纹理映射,就是研究如何将图贴上去的问题,而将图贴上去,用的方式就是采样。所以本文主要就是讲解纹理是如何采样的,以及如何用代码来实现这个采样过程

怎么采样呢?我们都已经知道在图形渲染管线中,在执行片段着色器之前,会进行光栅化操作,将图元切成一个个小片段,那么当我们需要把一张图片贴到图元表面的时候,本质就是将图片对应的纹理像素的颜色复制到我们所绘制的图元片段上的过程

image.png

那什么是纹理像素呢?

纹理像素

我们知道位图都是由一个个点组成的,如下图(当然这是夸大化的效果):

82cf5d9d91952382a8b17002c8b8caa8.jpeg

这里的每个点就是纹理像素(简称纹素)。每个纹理像素记录着相应的颜色信息,它类似于屏幕的 「像素(pixel)」但又与像素不保证是一一对应的

在电脑中我们可以查看每一张图片的尺寸,比如下面的图片(祭出女神图):

WeChat6f144825368b93f3d47a14d3b1033944.png

640*1138即为它的尺寸,它表达的含义就是该图片的纹素在横向和纵向的数量

为什么说“与像素不保证是一一对应的”呢?一般情况下如果按照原始尺寸显示一张图片,一个屏幕像素点会显示一个图片的纹素,但是我们可以放大一张图片在屏幕的显示很多倍,比如放大上图很多倍:

WeChat88dc6738a44316f509c989afed577982.png

可以清楚看到有一个个小方块,那就是一个纹素,但是屏幕的一个像素点肯定比这个小得多,所以在这种情况下,图片本身的纹素数量并没有改变,所以这里一个纹素是由多个屏幕像素显示出来的

那么可以更进一步推断出,要把这张图贴上去,其实只要我们绘制的图元的每个片段都去找到对应的纹素,将纹素上的颜色采到自己“身上”即可

所以要如何找到一个片段对应的纹素呢?

纹理坐标

假如现在我们要把石原里美的图

5578742e6aed7b62c54bd0ff375cf41a.jpeg

贴到如下图所示图元表面

image.png

要怎么做呢?

假如我们的图元的宽是a,长为b,为了方便定义当前片段在图元里的位置,我们给图元添加一个虚拟的坐标系,原点在左下角

WeChat436d77c9c8bc8125c50f46c6abb84daf.png

假如当前片段的坐标是(x,y),那么我们可以分别计算出x占宽度、y占高度的百分比m、n,然后可以在石原美里图也建立一个同样的坐标系,即左下角为原点的坐标系,按照比例取到对应的纹素的颜色

WeChat2f30d20e901ddf9e5083f1ebcd188f9c.png

我们已经知道这张图的纹素尺寸是640 × 1138,所以片段(x,y)对应的纹素坐标就是(640 * m,1138 * n)

但是有个问题,就是不同图片的尺寸是不一样的,我们需要统一起来才可以在代码中用一套坐标体系表示,所以这里又可以用归一化的思想,将坐标范围统一在0.0-1.0之间

9e0b8d4717e51beb2639146627c6cdb1.jpeg

坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。

这也叫做纹理坐标,通过纹理坐标,我们可以定位到纹理中的纹素

所以总结纹理映射的流程:遍历图形中所有的片段,依次通过片段所在的位置坐标定位到其对应在纹理中的纹素,再获取到对应的颜色

可能有人会问:“由于片段一般数量很大,所以遍历片段是一个很消耗性能的过程?”。

其实看过一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(三) 的童鞋就知道,借助gpu强大的功能特点,所有片段的操作都就是并行执行的,所以这里性能问题大家不用担心。

基于这样的映射定位关系,实际采样中,我们的纹理坐标个数需要和顶点坐标保持一一对应的关系

(左边为纹理,右边为图元)

70121b78915b568be7fa22ae90121bc1.jpeg

更通俗来说,这种贴图方式就是顶点一一对应起来,纹理的图片看成用橡皮泥做的(即可以进行拉伸),然后将纹理图片每个点拉到图元对应顶点位置贴上去

纹理环绕

有时候你可能会有更多需求,比如类似贴墙纸,需要重复一个图片很多很多次:

945c7fd89747d3ae3302c491d451bc9c.jpeg

或者会玩一些花哨的动作,比如玩重复的镜面对称或者一些边缘延伸效果,那么服务贴心的OpenGL也已经考虑到了,这也就是所谓的纹理环绕

(图来源 纹理

假如现在的纹理是:

WeChatbf29706d50ce1a3a38f3506d76e2db6b.png

则如果传入的纹理坐标范围超过1.0,则有以下4种可能效果:

image.png

4种效果从左到右依次是:重复、镜面对称、边缘延伸、超出部分显示指定颜色

具体指定环绕方式的代码,卖个关子,我们下篇文章具体讲代码的时候再讲。

0f458ec21e69c730b1202bf1ff2f1994.jpeg

纹理过滤

刚说完纹理环绕,怎么又出现个纹理过滤?

05de7ad3b897e2de23fd875f24688f94.jpeg

上面已经叙述了纹理映射的基本思路了,但是有个关键细节值得注意,就是当前片段定位到对应的纹理的纹素的时候,应该怎么赋值给当前片段?一定是直接取当前片段位置对应的纹素的颜色值么?

那倒未必~

如果是直接取对应的纹素的颜色值赋值给当前片段的情景,那么纹理映射后可能会出现的画面是:

WeChat365b796860262ea32ce72912e584099f.png

是不是清晰感受到了锯齿到来的诡异感?

ae33e335483d3dd1dd78526f038503ca.jpeg

为什么会这样呢?

因为要注意的一个点就是,片段和纹理上的纹素大小很可能不是一样大的,如果一个很大的图元去采样一个纹素低的纹理,就会出现一个纹素明显比一个片段大的情况,就会出现某个区域的很多片段对应的纹素是同一个,那么如果直接采那个纹素的颜色,就会出现某片区域的片段颜色都是相同的情况,也就是上图出现的锯齿问题

这就涉及一个纹理过滤问题,说白了就是一个片段具体如何采色的问题。

OpenGL最主要的纹理过滤方式有2种,一种是邻近过滤,Nearest Neighbor Filtering,就是刚讲的直接取对应纹素颜色的方式,另外一种是双线性过滤,(Bi)linear Filtering.

邻近过滤示意图

image.png

邻近过滤是OpenGL默认的纹理过滤方式,会选择距离映射坐标点最近的纹素的颜色。

双线性过滤示意图:

image.png

它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大,其实就是加权的叠加计算。

如下图所示,假如映射坐标点是红点,则会先根据周围4个纹素的中心点距离红点距离在纵向2排分别进行加权计算出2个颜色值,然后2个颜色值在横向用相同方式在加权计算出最终颜色值,所以叫做双线性过滤

2e4e917dd8db36cfeee5ab1fcf26652f.jpeg

使用双线性过滤的效果图如下图所示:

WeChat6061e67007e696bea3674dbb34548313.png

明显没有了锯齿,变得光滑了,不过也带来了新问题,变模糊了。所以这也是一个取舍问题。一般在图元尺寸比纹理大的时候采用双线性过滤,图元尺寸比纹理小的时候采用邻近过滤

纹理单元

如果我们绘制的图像只能贴一个纹理,即贴一张图片,那显示还是显得有点单调,如果能够贴上很多张图片,那才是一个五彩缤纷的世界。

所以OpenGL提供了纹理单元,通俗来说就是图层,这样我们就可以进行图层的叠加去产生一些有意思的效果

比如现在有2个纹理:

image.png

image.png

如果要将2个纹理映射到矩形的图形中,则需要激活2个纹理单元,然后一一将其采样过去即可:

image.png

一个纹理可以绑定多个纹理单元。当然纹理单元的数量也不是无限的,OpenGL规范规定最大图层数量最小为80,程序中定义的常量GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS表示最大纹理单元数量。

至于具体实现代码,还是那句话:下一篇文章见。

f3a13894c968b0b15835db0a73e513cf.jpeg

总结

本文主要详细讲解了纹理的概念以及纹理映射相关的理论知识,通过阅读本文相信各位已经对纹理映射相关理论了然于胸了吧。

e96199bd2fea6a8955430f8287e8fda0.jpeg

不过理论的东西总是让人觉得有些许单调和缥缈,只有落地到实践才能给与我们足够的充实感,那么下一篇博文,就来将纹理映射从梦想照进现实~

代码地址

opengl-es-study-demo (不断更新中)

参考

纹理
Texture

原创不易,如果觉得本文对自己有帮助,别忘了随手点赞和关注,这也是我创作的最大动力~

系列文章目录

体系化学习系列博文,请看音视频系统学习的浪漫马车之总目录

实践项目: 介绍一个自己刚出炉的安卓音视频播放录制开源项目

C/C++基础与进阶之路

音视频理论基础系列专栏

音视频开发实战系列专栏

轻松入门OpenGL系列
一看就懂的OpenGL ES教程——图形渲染管线的那些事
一看就懂的OpenGL ES教程——再谈OpenGL工作机制
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(一)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(二)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(三)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(四)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(五)
一看就懂的OpenGL ES教程——缓冲对象优化程序(一)
一看就懂的OpenGL ES教程——缓冲对象优化程序(二)
一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(理论篇)