![49a2b83ee930f66c56129c7d08e2e11c.png](https://i-blog.csdnimg.cn/blog_migrate/c5038ef00402cb4f203a03fbc7062f3e.jpeg)
纹理概述
在计算机图形学中,「纹理(Texture)」 是我们经常会提及的一个名词。
直观的第一印象,很容易会将纹理理解成一张图片,实际上在计算机图形学中,我们也确实会经常通过一张张格式诸如png、jpg之类的图片来获得纹理数据。
但在图形学中,纹理更多地是被认作 「一块数据」,它也不再局限在2D空间,也会有一维纹理、三维纹理、立方体映射纹理、数组纹理等。
纹理是由 「纹素(texel)」 构成的,每个纹素记录着相应的颜色信息,它类似于屏幕的 「像素(pixel)」 但又与像素不保证是一一对应的。
那么纹理为什么会被广泛使用呢?
这是因为,我们看到现实世界中所呈现出丰富的颜色,是由于不同物体会吸收一部分光的波长并反射出另一部分出来(如红色物体实际上是因为反射了大部分红色波长出来),最终通过眼睛传入大脑计算成像。而通过计算机去实时模拟这一过程无疑是复杂且耗时的,尤其是游戏要求在短短的一帧(少于0.016s)中就完成一次场景渲染。
为了解决这个问题,人们想到了一个办法:找到一张图片(离线渲染生成),然后把它“粘”在物体的表面上,就像贴墙纸一样。而这一“粘”的过程,我们管它叫做 「纹理映射(texture mapping)」,实际上也就是一块纹理数据与3D模型建立关联的过程。
看过武侠小说的应该都听说过“易容术”,将不同的“人皮子”贴在脸上,就可以易容成别人的模样。其实这就是一个简单的纹理映射的过程。
![865312e2436c8896a05b8ed36c95a92f.png](https://i-blog.csdnimg.cn/blog_migrate/de5a90d6da83b9b233e8526f9e53006b.jpeg)
有人可能会有疑问,每个人的面部肌肉都不尽相同,仅用一张纹理是怎么做到“易容”的呢?卖个关子,后面在纹理的应用中会有介绍。
纹理映射原理
在渲染管线中,一般在 「片元着色阶段」 最常使用纹理。
在美术建模时,会为每个顶点分配一个纹理坐标,也被叫做UV坐标。在进入光栅化阶段,生成的每个fragment会根据相关顶点的纹理坐标差值后新的uv,根据这个uv来对传入的纹理数据进行「采样(使用纹理坐标获取纹理颜色)」。
![5e04110d9f36610248289766f62ffff8.png](https://i-blog.csdnimg.cn/blog_migrate/2af0a944696bc590ea7f682f46ecc1ac.png)
根据上图说明一下简单的纹理映射,某个三角形包含三个顶点,对应记录的uv坐标分别是(0,0)、(1,0)和(0.5,1)。光栅化之后,每个小格子就是一个fragment,对应屏幕的像素。在这个三角形区域中的每个像素,会根据顶点所记录的纹理坐标来进行差值计算,这就涉及到了三角形内的重心坐标计算。
![f11cf8be71319f11c2aa4d9f3d5ca2a2.png](https://i-blog.csdnimg.cn/blog_migrate/5428c1f3b52c2167d07596669b73d8b0.jpeg)
如上图,已知三角形ABC内任意一点(x, y)以及三个顶点ABC的位置,即可求出该点相对三角形的重心坐标位置。进而可以计算出该点所在像素对应的UV坐标:
一般来说,纹理坐标的范围都是[0, 1],但在某些情况下,可能会使用超出该范围的结果。例如墙面纹理,我们可以只制作一块砖的纹理图片,在根据模型实际宽度来放大顶点纹理坐标,使得可以循环重复采样同一块砖的纹理。这就涉及到了设置「纹理环绕方式」,在OpenGL中,常见的选择如下:
![428708faa7d4925ac6337ede3c7d976c.png](https://i-blog.csdnimg.cn/blog_migrate/d0d8e3d4ba25fd8738049c2fec5e9a34.jpeg)
![14a26aefbc71f5dfd9818ff1ea614268.png](https://i-blog.csdnimg.cn/blog_migrate/4b23bf6bf5fdd39b96e6e5aa7379bd02.jpeg)
还有一种比较常见的灵活纹理采样方式,就是UV序列帧动画。美术会做好一张 N * N 的图片,最最常见的就是火焰效果了。
![0ee47cc1908e85aedf070eb5caf729e6.png](https://i-blog.csdnimg.cn/blog_migrate/70df836c968154b512900dde5e60805c.jpeg)
每次在片元着色器对该纹理进行采样的时候,根据一定的频率来调整修改需要传入的UV坐标,使得每次恰好只截取展示其中的一个tile,连续起来会实现出帧动画的效果。
说到这里不难发现,跟纹理打交道,本质上就是确定「数据内容」和「采样方式」。在片元(像素)着色器中,根据UV坐标对纹理数据中对应纹素的颜色进行提取,再渲染到像素中去。但是,这仅仅是最简单、理想的情况。无论是纹素还是像素,你都可以理解为它们是有「大小」的,只不过人眼不易察觉罢了,毕竟现在手机一般都是1920 * 1020分辨率起步的,还是仅仅在一块6寸大小的空间下。
当纹素与像素的大小比例过于悬殊时,会出现一些明显的“锯齿”现象,下面会从两种情况分别介绍。
纹理过小
假设一张 480 * 270 尺寸的纹理要铺满渲染到分辨率为 1920 * 1080 的屏幕上,此时一个纹素的大小可以理解为是像素的4倍。而在片元着色阶段,默认会根据差值计算生成的纹理坐标,找到离该片元最近的纹素,并采样提取其颜色。
![2741c7d54b56c175af61854e4fe5eaf3.png](https://i-blog.csdnimg.cn/blog_migrate/7008108d6e94528ddd2efb1da9031f2c.jpeg)
如图,红色点是我们实际想要采样的纹理坐标(由于像素更小,所以采样点会更加精密),而黑色是实际上采样到的纹理坐标对应的颜色。所谓的「锯齿(aliasing)」,就是这么来的:紧挨红色点的右侧和上方,会跟着采样出同一纹素的颜色,但是离红色点更近的左侧部分,却只能采样到相邻左侧纹素的颜色。离着相对较远的两个像素,可以采样出同一种颜色出来,而明明离得更近的两个像素,却只能采样出两个纹素的颜色(有可能颜色差异比较明显)。
那么如何解决这个问题呢?
答案就是:「双线性过滤(Bilinear Filtering)」
先简单介绍一下线性差值的计算,几乎在计算机图形学中经常用到,就是已知 v0 和 v1 两个值,以及一个线性比例,求出该线性比例下对应的值,具体公式如下:
所谓的双线性过滤,就是根据某一像素的纹理坐标,找到其周围四个纹素的位置并采样其颜色,通过横向、纵向分别进行差值计算,得到最终的颜色。
![289c4ad1399a746e2cf6fc3e6f19fa75.png](https://i-blog.csdnimg.cn/blog_migrate/3554ace79f91b429fba9184a10922bdb.jpeg)
上图就展示了双线性过滤的基本过程:先找到红色采样点周围的四个纹素位置,采样得到4个颜色分别为 u01、 u11、 u00、 u10 ,先横向根据 u01 和 u11 线性差值得到 u1 ,u00 和 u10 线性差值得到 u0,再通过 u1 和 u0 进行线性差值得到最终的颜色。
这样的双线性过滤得到的效果,较之前的 「邻近过滤」 方式,会呈现出更加平滑的颜色过渡,“锯齿感”明显减少了。
![38aa2bdc552914a82c454ce5d97a04b6.png](https://i-blog.csdnimg.cn/blog_migrate/28cc1b3d5d04628bbaa000acb8d0bd50.jpeg)
纹理过大
说完纹理过小,我们再来讨论下纹理过大的情况。也许你心中会有疑惑,纹理过大也不行?
其实道理类似,纹理过大,每个纹素的大小相较于像素更小了,就导致每个像素覆盖的区域实际上会包含多个纹素,但在片元着色阶段只会进行一次「点采样」,得到的结果显然是不够“精密”的。从信号学的角度上来理解,每个像素的采样频率远远低于纹理上的变化频率,于是也会形成锯齿的效果。假设一张特别大的纹理贴在地面上,由于透视摄像机的近大远小原理,会放大像素与纹素的信号频率差距,因此远处的地面会形成一种叫做“摩尔纹”的效果。
![ffe4867ebd61207cbc1f1a18566e7131.png](https://i-blog.csdnimg.cn/blog_migrate/008ed75e31a73dd5eb57ff9f64352aa0.jpeg)
既然是因为采样频率不够,那么解决这个问题一个最直观的办法就是 「超级采样(Super Sampling)」 ,每个像素只采样一次不足以覆盖包含的所有纹素的话,那就多采样几次,并将最终的采样结果中和到一起,生成该像素的最终颜色。
超级采样虽然的确能以较高的质量解决锯齿问题,但计算成本实在是太高了!尤其是很远处的像素,为了抗锯齿进行了大量的多重采样,人眼看到的结果却无法辨别细节,这无疑是一种严重的浪费。(当然,如果性能条件允许,这么做也无可厚非。)
为了能够高效获取某一像素下对应纹理某一区域的颜色,图形学中的常见解决方案是:将一块纹理生成一系列的纹理图像,其中后一个纹理图像是前一个大小的二分之一,然后进行范围查询。这种解决方案叫做 「多级渐远纹理(Mipmap)」 ,其中 Mip 一词源于拉丁语,意为 一小块空间下许多细小的东西 (multum in parvo) 。
![d4355fe0298c6eebe687e05ebc0fb1d1.png](https://i-blog.csdnimg.cn/blog_migrate/8d8675a86475103c42608de1ea8646fa.jpeg)
Mipmap是一种典型的空间换时间的解决方案,通过「范围查询」来替代之前双线性差值所做的「点查询」,它的好处就是速度快,但是也有缺点:结果不够准确、只能用于正方形(2的幂次方)贴图,以及增加了额外的存储量(增量约为1/3)。
我们如何确定该使用那一层的Mip呢?道理其实很简单:
![39bc93256d084a53bf4a3c156f0b18be.png](https://i-blog.csdnimg.cn/blog_migrate/b63ff890a007d3cfe450643c25cb7569.jpeg)
假设一个三角形覆盖屏幕一定区域的像素,其中选择一红色的采样点以及其相邻的3个采样点(都是红色),映射到纹理坐标系下,求出两个像素采样点在纹理坐标系下的近似长度 L 以及一块粉红色的区域。根据这块区域的近似面积以及纹素的大小,就可以得出该使用哪个层的Mip了。
比如:
如果正好是一个纹素的大小,也就是一个像素对应一个纹素,那么就选择第0层的Mip
如果对应四个纹素的大小,也就是一个像素对应4个纹素,那么就选择第1层的Mip
以此类推,得出上图中出现的公式:
当计算出的层数恰好不是整数的时候(事实上经常会出现这种情况),同一纹理上的不同Mip之间会出现明显的断层现象,如下图底部的红色区域,深红色直接过渡到鲜红色,十分地不自然。
![afb93ca7e08d7365b7c91f00f8c68ca6.png](https://i-blog.csdnimg.cn/blog_migrate/cc4137c9c25ce8eea8ccf006298ecde7.jpeg)
出现了这种不自然的过渡,解决办法依然是老办法:「差值」 !
假设计算出的Mip层数是0.7,那么需要将该采样点在第0层和第1层分别进行双线性差值,然后再将两个Mip计算出的颜色结果再进行一次线性差值,得到最终的颜色。我们管这种计算方法叫做:「三线性差值(Trilinear Interpolation)」。
下图是使用三线性差值后的表现,是不是平滑了许多?
![511943a9c8804ff49fbfa27a59ad3747.png](https://i-blog.csdnimg.cn/blog_migrate/8b5b17453613c41146452f30ff20617d.jpeg)
但是某些情况下,使用三线性差值计算出的Mipmap颜色依旧会有问题。类似开放世界的第一人称视角,看向地平面很远处的位置,可能会产生出over blur的效果。还记得Mipmap的缺点吗?它只能针对正方形纹理进行Mip分层,当透视效果越明显,越远处的像素对应纹理中的区域会越不接近正方形(更像长条的矩形,甚至连矩形都不是),如果使用面积更大的正方形来包住这个区域,此时的结果就会愈加不准确,造成了越远处越模糊的感觉。
为了解决这个问题,很多硬件都支持 「各项异性过滤」的特性,例如OpenGL中可以通过 GL_TEXTURE_MAX_ANISOTROPY_EXT 来设置纹理中各项异性过滤的数值。
![49ae8adcf0e680ab421bd5d36d130de8.png](https://i-blog.csdnimg.cn/blog_migrate/e9ece9a35fe1cd56548ebf0485ddaece.jpeg)
虚拟纹理
读到这里,相信你已经对纹理映射有了更清晰的认知。在现实游戏开发中,为了有更好的视觉体验,往往会使用巨大的纹理加Mipmap的形式来表现物体,但这就会给带「显存宽带」来压力。于是,「虚拟纹理」应运而生。
虚拟纹理的思路与LOD Mesh的思路比较接近,就是不会一次性将整张巨大的纹理加载到内存中,而是根据实际需要,只将需要的部分进行加载。实现方法就是,将每层Mip拆分成大小一致的tile(虚拟纹理),根据某种规则,映射到内存中存在的纹理(「物理纹理」)中。当视野发生变化的时候,物理纹理中的一部分会发生变化,重新映射相应虚拟纹理中的内容。
![6e059973b89e4174773d292cdcf78118.png](https://i-blog.csdnimg.cn/blog_migrate/b4f853d48a37c47bb25b22f8bb9c22a4.jpeg)
关于虚拟纹理,这篇文章十分推荐阅读,以及关于虚幻4中虚拟纹理的源码导读。
纹理应用
纹理在实际应用当中,远不止贴上“一层皮”那么简单。通过各种各样的纹理,我们可以实现出更加逼真、高效的表现来。下面简要介绍一些比较常见的纹理贴图应用。
更多详细的介绍,请参考这篇文章。
Bump/Noramal Map
有的时候,纹理不只是用来存储颜色,也可以存储一些高度、法线等向量信息,以用来在一个平整表面来展现出一些细节上“不平整”的trick效果。
我们管这种贴图叫 「凹凸贴图」 或 「法线贴图」。
![147069599733967973433eec3c77601d.png](https://i-blog.csdnimg.cn/blog_migrate/a796a6cfed41164a04acc876fa950eab.jpeg)
一般的法线贴图,都会呈现出一种偏蓝色的效果。这是因为平面上的法线向量为(0, 0, 1) ,正好对应 RGB 中的 「B」 通道。法线贴图只是对不同平面上的法线进行轻微的扰动,造成光线反射到人眼(摄像机)的方向发生变化,从而营造出一些明暗变化的细节表现。
下面是没有使用法线贴图与使用法线贴图的对比效果。
![613b88d02cd605b2c0f29a95886f8d4e.png](https://i-blog.csdnimg.cn/blog_migrate/4fbd209a86edcb2e1e24e77917fe694f.jpeg)
Light Map
对于一些相对静态的场景,实时地去计算复杂的全局光照无疑是十分影响性能的,尤其是在移动平台。于是,先离线渲染出一张场景的光照贴图,里面包含了当时场景复杂的光照计算结果,通过与场景的漫反射贴图相结合来生成一个不错的光影表现效果,大大节约了性能开销。唯一的缺点就是,如果镜头或光源在Runtime时发生动态变化,很容易穿帮。
Cube Map
立方体纹理由6块方形的纹理组成,分别对应一个立方体的6个面。立方体贴图有一个非常重要的作用,就是可以根据一个方向向量来进行采样。
常见的一种做法,利用Cube Map制作出一张环境光的反射贴图,并将一个模型包起来,根据模型表面的法线采样Cube Map对应的纹素点,将得到的颜色与模型Diffuse贴图的颜色结合计算,得到一个相对逼真的模拟环境光反射效果。模型在这个Cube下的自旋转,表面的光影也会随着发生相应的变化。只不过与使用烘焙出来的Light Map类似,当镜头或光源发生变化时,还是容易出现穿效果,也只是一种降低性能开销的取巧办法。
参考引用:
GAMES101-现代图形学-闫令琪www.bilibili.com 纹理-LearnOpenGL CNlearnopengl-cn.github.io![153d66cb89c3fd6745404bfcd3cf71e4.png](https://i-blog.csdnimg.cn/blog_migrate/1331a6eee25d30fa4479063d0716bbd6.png)