【RTR4笔记】 第六章 纹理

纹理管线以及各种贴图

Posted Kongouuu's Blog on January 26, 2022

6.1 纹理管线

我们会经常用到一个纹理来对一个物体的材质(包括颜色)进行形容,而纹理的使用本身也形成了一个固定的纹理管线。

  1. Projector Function: 首先我们会把本地坐标通过这个投影函数变成纹理坐标

  2. Corresponder Function: 我们是没办法直接通过给定的一个纹理坐标(u,v)去贴图上找到数据的,我们需要先通过这个映射函数把纹理坐标转换到纹理空间中去搜索对应的像素
  3. Obtain Value:通过上个阶段得到的输入,我们可以正确的读取像素
  4. Value Transform Function: 在这个阶段我们会把得到的数据转换成需要的使用格式

6.1.1 投影函数

比较基础的情况下我们的纹理的索引会都已经写好在模型的顶点数据里面。所以正常的情况下我们是并不需要去进行什么投影函数的操作的。

但是假设我们是要去实现一些特殊的光照效果,例如说环境光照;那么我们就需要基于物体的物理信息去计算该怎么得到贴图的数据,这个流程就是投影函数。这是一个比较理论上的管线阶段,并不是一个独立出来的GPU处理的阶段。

6.1.2 映射函数

一般来说我们的纹理会采样在[0,1)的范围,但很多时候我们会有超出这个范围的纹理坐标的查询。这时候我们就需要用映射函数去决定超出范围的值怎么处理:

  1. 重复寻址(warp): 图像在表面上重复
  2. 镜像寻址(mirror): 图像在表面上重复,不过每次重复会对图片翻转
  3. 夹取寻址(clamp): 超过范围的采取边缘的值
  4. 边框寻址(border): 超过范围的使用额外设置的边框值

6.2 图片采样

6.2.1 放大

当我们要把一个图片纹理映射在大于它分辨率的表面上的时候,难免会有失真。

为了减少失真的影响,我们会采取双线性插值来决定一个像素的颜色。双线性插值是对于一个屏幕上的像素,去采样使用的最近的四个纹理元素,并且找到他们的插值。

6.2.2 缩小

我们如果要把一个纹理渲染到一个较小的表面上,那么会出现一个问题,就是一个像素可能会覆盖多个纹理元素。

这里我们没有办法实时的去算出有多少纹理元素被像素覆盖,所以很难去计算最后的颜色。如果使用最近的纹理元素,不进行插值,最后的结果可能会有很大的走样。双线性插值在这个时候也不好用,因为有可能会不止四个纹理元素被一个像素覆盖。

为了防止这样的走样发生,有三个比较主流的解决方案:

6.2.2.1 Mipmapping

Mipmapping 就是预先的为一个纹理生成好几层均值后的图像。每一层压缩都是上一层的1/4的大小,并且每个纹理元素都是上一层的四个纹理元素的均值。

这样的预先处理只需要额外的33%的开销就能达到。然后只需要在处理中去预估一个像素涵盖了多少纹理元素就可以了,其中一个做法是拿体素沿着像素的缩小率去计算处于哪一个层级。

6.2.2.2 Summed-Area Table

这是一个比较特殊的数据结构,我们将用这个结构来储存我们的纹理。

SAT的结果就是里面的每一个纹理元素的值,都是当前元素跟最左上的元素形成的长方形里面所有的元素之和。我们可以使用SAT找到一定范围内的纹理元素的和,以及他们的平均值,通过这个手法也可以解决像素覆盖大量纹理元素的问题。

6.2.2.3 Anisotropic Filtering 各向异性过滤

这个方法相当于Mipmapping的升级版。我们知道Mipmap每一次的分辨率都是上一层的1/4,这个问题在如果我们的表面并不是对称拉伸的情况,而是只拉伸X/Y其中一个的时候,那么Mipmap的结果会过于模糊。

也很好理解的,只需要吧不同比例的拉伸的压缩后的纹理也都提前计算好就可以了,例如用1x2, 2x1这样的比例去压缩。好处是这个不会有Mipmap的过度模糊的问题,不过显存开销特别的大。

6.2.3 体积纹理

一个2D的纹理一般是一个图片,为了表达一个3D的体积,一般来说就是采样多个图片的叠加。使用这样的手法可以在对三维的网格上有更加灵活的操作,不过代价是插值的开销特别的高。

6.2.4 立方体贴图

立方体贴图算是六个2D纹理的集合,把六张纹理拼成一个方块的操作。这样的贴图可以作用于我们的天空盒绘制,并且在计算环境光照的时候也十分的有效。

6.2.5 纹理的表现方式

很多时候我们在渲染中会使用大量的纹理。为了让GPU更好的处理这样的情况,我们有了Texture Atlas这个手法,也就是把大量的贴图整合到一张大贴图里。这样只需要传一个大贴图到GPU就可以达到使用多个贴图的效果。

现在的API比较高级,可以使用纹理数组这样的结构来一次传递大量贴图。纹理数组内部的纹理需要都是使用相同的维度、格式、Mipmap层数。这样的数据结构会比单独绑定每个贴图快上许多。

传统的纹理数组的缺点在于绑定的开销和限制,所以我们如果要传不同的纹理还需要在额外绑定其他操作。现代的API推出了一个叫Bindless Texture的功能,只要在GPU里面存有纹理的信息,就可以自由的通过句柄在Shader内读取纹理信息。通俗点说就是不需要绑定的开销,也可以同时根据传入的句柄去随便的读取不同格式不同大小的纹理,也不设读取上限。

6.2.6 纹理压缩

todo

6.3 过程纹理 Procedural texture

过程纹理,从英文字面上来看就是程序化生成的纹理。一般会用于我们生成一些自然元素,例如石头等的表面。通过程序生成的噪音可以保证不同的石头有不一样的凹凸以及表面纹理。

这样的处理可以让我们能够自动的生成大量的不重叠的一些小物件。不过问题在于纹理的生成(包括噪音的生成)都是想当消耗时间的,因此大部分时候这个功能都是用来做离线渲染,或者是说为实时渲染提供图片纹理。

当然,也有很多情况下我们会用到程序化生成的纹理,例如物理模拟结果的水波纹。不如说很多的物理模拟的结果我们都是可以用来生成一些过程纹理的,来达到一些普通的图片纹理没办法达到的互动效果。

6.4 纹理动画

我们的Shader在采样纹理的时候是不需要每一帧采样相同的元素的。我们可以手动的让表面的每一帧采样贴图的不同元素,来达到动画的效果。

6.5 其他贴图

6.5.1 凹凸贴图 Bump Map

凹凸贴图记录的信息是高度。使用的时候我们会根据像素附近的纹理元素的高度值计算出当前的法线方向,来达到凹凸的效果。

没有改变顶点的位置。

6.5.2 位移贴图 Height Map

类似于凹凸贴图,不过会实际的更改顶点的坐标。这里有一个比较大的问题是如果我们需要适应一个位移贴图,那么我们的表面需要大量的顶点,否则没有办法达到位移的效果。

6.5.3 法线贴图 Normal Map

法线贴图算是一个升级版的凹凸贴图吧,存储的是我们希望的法线方向。所以他的作用就是相较于凹凸贴图,更加灵活地去改变法线的方向。

这里的法线方向是基于TBN坐标进行的,也就是顶点的切线空间,所以为了使用法线贴图我们还需要传入Tangent以及Bitangent信息。实际上我们确实可以把法线贴图的点记录为局部空间,不过这样的泛用性会十分的糟糕。

用局部空间的话,然我们可以写入一系列的法线向量,然后再根据物体整体的一个Model变换(主要是旋转)去进行方向的转变。不过这样的操作就相当于我们需要给所有的模型都独立的计算一套法线贴图,并且不能泛用。如果我们有一些小饰品,例如手环,是可以合并到很多模型上的;但如果我们使用局部空间写法线贴图,那么同一个手环在不同模型上都需要重新设定法线贴图,这是很不合理的。简单来说就是缺乏局部性,假设物体形变了也会失效(例如物体的一部分扭曲,但并不是作为整体进行拉伸/旋转)。

首先几何体本身是能有几何体的法线的,也可以计算出切线。那么比起针对整个几何体去定制一个法线贴图,我们可以把几何内的每个三角形看作一个面,然后根据面的几何法线方向去定制我们的法线贴图所需要提供的信息,也就是TBN空间。

6.5.4 视差贴图 Parallax Map

视差贴图也是凹凸贴图的升级版,虽然learnopenGL写它是位移贴图的一种,但其实视差贴图是不改变顶点的。

从数据的角度来看,视差贴图跟凹凸贴图是一样的,都只是记录深度,不过两者对深度的处理是不一样的。视差贴图最主要的目标就是,根据给定的深度调整图来计算应该读取哪一个纹理元素。

上图里我们通过视线(黄色的线)看向了我们的表面。正常,以及凹凸贴图的计算中,我们这时候会展开纹理元素A的点计算。这样的结果并没有考虑到高度改变带来的遮挡效果;在视差贴图的应用中,视差贴图会计算出A被B挡住了, 然后使用B点的颜色去计算最后的成色。

简单来说视差贴图的作用就是通过一个调整深度的贴图来决定颜色和法线应该真的采样的纹理元素(Texel)。

6.6 光源以及其他处理

纹理贴图同时也是可以用来代表我们的光源的。因为纹理贴图里面可以储存三个颜色[0,1]范围的值,所以可以很好的进行光照的模拟。

最简单的例子就是用一个Cube Map当作环境贴图的光照来源。一般来说如果想要一个更好的环境光照效果,这样的手法是不错的,虽然还需要更多的处理(PRT)才能进一步提升效果。除了使用Cube Map外,体积纹理也可以用来做更进阶的光照效果,后面的章节会提到。

除了光照以外,纹理本身非常适合用来当作一个数据的存储。拿简单的来说就是比如我们对阴影或者环境光遮蔽的效果的数据存储,复杂点比如蒙特卡洛补正等数据都可以储存在纹理。有很多的数据都是我们可以通过一个纹理的格式传到Shader里面作用于计算的。在本书的后期应该也会提到很多类似的Trick