《Real-Time Rendering》第六章 纹理映射

开篇

  在计算机图形学中,纹理映射是一个过程,它会在表面的每个地方使用一些图像、函数或是其它数据来修改表面的外表。比如,与其精确地表示砖墙的几何结构,不如把一张砖墙的图像“贴”(映射)到一个矩形上,让其看起来真的像一堵砖墙那样。除非观察者靠近观察矩形,否则不会察觉缺失的几何细节。
  然而,纹理映射后的砖墙除了缺失几何细节外,还有一些原因让它看起来不那么真实。例如,砂浆是哑光的(不光滑的)而砖块是光滑的,观察者会发现两种材质有着相同的粗糙度。为了让砖墙看起来更真实一些,另一个图像纹理可以被映射于表面。这个纹理不会改变表面的颜色而是改变墙的粗糙度。现在砖块和砂浆会分别有来自两个纹理的颜色和粗糙度。
  观察者现在会察觉到砖块都是光滑的而砂浆不是,但仔细观察也会注意到砖块表面看起来是过度平整的。以实际生活来看这是不对的,因为砖块表面通常都是不平整的。通过应用凹凸映射Bump Mapping),我们可以让着色法线变动,这样表面就不会表现得完美平滑。这种类型的纹理会改动矩形的原始表面法线,被改动的表面法线接着可以被用来着色。
  以掠射角来观察,凹凸映射带来的凹凸感将会有问题。因为,砖块实际上应该突出在砂浆外。此外,以接近正视的视角来观察,砖块应该向砂浆投射阴影。视差映射Parallax Mapping)使用了一个纹理,它能让平整的表面表现得像被变形了一样。此外视差遮蔽映射Parallax Occlusion Mapping)会投射光线到高度场纹理上用于改善真实感。位移映射Displacement Mapping)则会真正地移动表面,它会修改三角形的高度。下图展示了颜色纹理映射和凹凸映射的一个例子。

img

  以上这些是可以用纹理和更多更复杂的算法来解决的问题。在这一章中,一些纹理映射技术将会被详细讨论。首先,一个一般的用于纹理映射流程的框架将会被展示。接下来,我们专注于将图像映射于表面这个话题,因为这是纹理映射用于实时图形最受欢迎的一种形式。过程式纹理将会被简要地讨论,接着一些通用的让纹理影响表面的方法将会被解释。

纹理映射管线(The Texturing Pipeline)

  纹理映射是一个技巧,它能高效地建模表面上随位置变化的属性。一个用来思考它的方法是想一想对于单个被着色的像素发生了什么。正如上一章了解的那样,着色要考虑到材质的颜色、光源还有一些别的因素。透明度也会影响像素的结果。纹理映射会修改被用于着色方程中的值。这些值通常都是随表面上的位置变动的。对于之前的砖墙例子来说,表面上任意一点的颜色会基于表面位置,被砖墙图像上对应的颜色替代。图像纹理中的像素通常被称作纹素Texture ElementTexel),这与屏幕上的像素区别开来。粗糙度纹理会修改粗糙度值,而凹凸纹理会改变着色法线的方向,这些改变最终会对着色方程的结果造成影响。
  纹理映射可以通过一个广义的纹理管线来描述。有些术语将会被介绍,不过请放心,这个管线的每个部分都会被详细介绍。
  空间中的一个位置是纹理映射过程的出发点。这个位置可以是世界空间,但是更通常在模型自己的坐标系中,当模型移动时,纹理也会随之一起移动。使用Kershaw的术语,空间中的这个点有着一个投影Projector)函数被应用,来获得一系列被称为纹理坐标Texture Coordinate)的数字,这些数字会被用来访问纹理。这个过程就叫映射Mapping),更进一步就有了纹理映射Texture Mapping)这个短语。有时,纹理图像它自己被称为纹理贴图Texture Map),当然这不是严格正确的。
  在这些值被用来访问纹理前,一个或更多的对应Corresponder)函数会被使用来变换纹理坐标到纹理空间的位置。这些纹理空间的位置接着会被用来真正地从纹理中读取数值,它们可以是数组索引来取回图像纹理中的一个像素。被取回的值有可能被一个值变换Value Transform)函数变换,这些新值最终会被用来修改表面的一些属性,比如材质或着色法线。下图详细地展示了映射单个纹理的过程的细节。

img

一个有这样复杂度的管线可以在每一步为用户提供有用的控制。在这里要注意,并不是所有步骤都得同时开启的。
  下图展示了使用这个管线,将砖墙纹理映射于一个三角形时,发生的一次采样过程

img

\((x,y,z)\)为物体本地坐标系内的一个位置,假定它为\((-2.3,7.1,88.2)\)。一个投影函数接着应用于这个位置。就像世界坐标被投影到二维平面上,投影函数会把\((x,y,z)\)转变为\((u,v)\)。用于这个例子的投影函数会等价于一个正交投影函数。假设获取的uv值为\((0.32,0.29)\),接下来我们就要用这个纹理坐标来找到图像在这个点的颜色。假设砖块纹理的分辨率为\(256 \times 256\),那么对应函数应该把输入的\((u,v)\)乘以\(256\),因此得到了\((81.92,74.24)\)。抛弃小数部分后,我们就得到了要访问的像素\((81,74)\),它的颜色为\((0.9,0.8,0.7)\)。纹理的颜色处于sRGB颜色空间中,如果这些颜色要被用于着色,那么它们首先得被转换到线性空间,转换后为\((0.787,0.604,0.448)\)

投影函数(The Projector Function)

  纹理映射过程的第一步是获取表面位置,并将其投影到纹理坐标空间,它通常是二维的\((u,v)\)空间。建模工具通常允许美术师逐顶点定义uv坐标。这些坐标可能初始化自投影函数或是网格展开算法。美术师可以像编辑顶点位置那样编辑uv坐标。投影函数通常会把三维空间中的点转换为纹理坐标。在建模程序中通常被使用的函数包括球面、柱面、平面投影。
  投影函数也能使用其它类型的输入。比如,表面法线可以被用来决定六个平面投影方向要用哪一个。不过这里要注意在每个面相接的地方会有缝隙。Geiss讨论了一个进行混合的技术。Tarini等人描述了多立方体贴图Polycube Map),在这个技术中,模型会被映射到一系列立方体投影空间中,不同的空间区域会映射到不同的立方体
  其它的投影函数则根本就不是投影,而是表面创建和镶嵌细分的一个隐含部分。例如,参数曲面会有一系列作为它们本身定义的uv值。下图展示了纹理坐标也可以使用一些不同的参数生成,比如观察方向、表面的温度等等其它能想到的参数。

img

投影函数的目标是生成纹理坐标。那些从位置得到纹理坐标的方法只是其中一种方式。
  非交互式渲染器通常让投影函数作为渲染过程的一部分存在。单独一个投影函数对于整个模型来说也许是够用的,但是美术师通常会对模型进行划分,对不同的部分应用不同的投影函数。下图是个相关的例子

img

  在实时图形中,投影函数通常会在建模的阶段被应用,投影的结果会逐顶点进行保存。然而还有一些别的情况。有时候在顶点或像素着色器阶段应用投影函数是有好处的。这么做可以增加精度,而且可以实现不同的效果,就比如动画。有些渲染方法,比如环境映射Environment Mapping)有会专门的投影函数,这些函数会逐像素被评估。
  球面投影会投射点到一个虚拟的球上。这个投影和Blinn与Newell的环境映射方法中使用的投影一样,书中407页描述了这个函数。但是这个投影方法会受制于407页的那个部分描述的顶点插值所遇到的问题。
  柱面投影的u纹理坐标的计算会和球面投影一样,而v纹理坐标的计算要用到沿着圆柱轴线的距离。这个投影对于有着一个自然的轴线的物体来说很有用,就比如旋转曲面。畸变会发生在表面和圆柱轴线近乎垂直的时候。
  平面投影和X射线束相像,它会沿着一个方向进行平行投影,并对所有表面进行纹理映射。它使用的是正交投影。这种类型的投影对于贴花(decal)来说很有用。
  因为严重的畸变通常发生在表面与投影方向几乎平行的一些地方,美术师通常必须手动分解模型到一些几乎平整的部分。也有一些工具可以通过展开网格或创建一个几乎最优的一系列平面投影或其它方法来最小化畸变。目标是让每个多边形在纹理中有合理大小的面积,同时尽可能维持网格的连通性。连通性很重要,因为采样伪影会发生在使用纹理不同的部分的两个表面相接的地方。一个有着好的展开的网格通常会减轻美术师的工作。Section 16.2.1就讨论了纹理畸变会如何对渲染造成不利的影响。下图展示了用于创建上图雕像的工作空间。这个展开的流程是一个更大的研究领域的一部分,它叫网格参数化Mesh Parameterization)。感兴趣的读者可以去参考下Hormann等人的SIGGRAPH课程笔记。

img

  纹理坐标空间并不总是二维平面,有时候它也可以是一个三维体。在这种情况下,纹理坐标是用\((u,v,w)\)来表示的,其中\(w\)指代在投影方向上的深度。另外一些系统使用了四个坐标,通常由\((s,t,r,q)\)来指代。其中\(q\)被用于齐次坐标的第四个值。它会表现得像电影放映机或幻灯机那样,投影的纹理会随着距离的增加而增大尺寸。比如,这对于投影一个被称为灯光模板Gobo)的装饰性聚光灯图案到舞台或是其它表面来说很有用。
  纹理坐标空间的另一个重要类型是方向,这个空间中的每个点都是通过一个输入的方向得到的。一个用来可视化这个空间的方法是想象单位球上的点,球面上的点的法线会作为方向被用来访问纹理在那个位置的值。最常见的使用方向参数化的纹理类型是立方体贴图Cube Map)。
  同样也值得注意的是,一维纹理图像和函数也有着它们的用途。比如对于地形的渲染来说,颜色会取决于海拔。低地会是绿色的,而山峰会是白色的。线条也可以使用纹理映射,比如可以使用一个半透明图像来渲染雨水。这种纹理对于从一个值到另一个值的转换也很有用,可以作为一个查找表。
  因为多个纹理可以被应用于表面,多个纹理坐标集也许需要被定义。当给予顶点纹理坐标时,纹理坐标会在表面上被插值,插值后的纹理坐标接着会被用来取回纹理上的值。在被插值前,这些纹理坐标会被对应函数变换。

对应函数(The Corresponder Function)

  对应函数会把纹理坐标转换到纹理空间的位置。这个函数有一定的灵活性。一个对应函数的例子是使用API来选择一个纹理的某个部分用于显示,只有这个子图像将会被用于后续的操作。
  对应函数的另一个类型是矩阵变换,它可以在顶点着色器或像素着色器阶段被应用。这得以实现平移、旋转、缩放、剪切或投影纹理到表面上。正如我们所知的那样,变换的顺序是很重要的。而出奇的是,纹理变换的顺序会和你所想的顺序相反。这是因为纹理变换所影响的空间决定了图像的哪个地方被看到。图像它自己不是一个被变换的物体,而是定义图像位置的空间在被变换。想象一下有个星空纹理,如果让纹理坐标向左移动,实际观察起来会发现星空在向右移动!
  另一类对应函数控制了图像被应用的方式。我们知道当uv坐标处于\([0,1]\)范围时图像会出现在表面上。但是如果超出了这个范围呢?对应函数就决定了越界处理这一行为。在OpenGL中,这种类型的对应函数被称为“环绕模式”。在DirectX中,它被称为“纹理寻址模式”。下面有一些常用的这种类型的对应函数

  • 环绕Wrap)DirectX,重复Repeat)OpenGL,:图像会在表面上不断重复。从算法上来讲,就是抛弃了纹理坐标的整数部分。它能让一个材质的图像在表面上重复,通常也是默认的选项。

  • 镜像Mirror):图像会在表面上不断重复,但是是镜像的,每次重复都会被反转。下面举个例子,假设纹理坐标从0增长到了3,从0到1时会正常地进行采样,从1到2的采样会对应着从1到0(与从0到1相反)的采样,从2到3时又会回到从0到1的采样。它能在纹理边缘处提供一些连续度。

  • 钳制Clamp)DirectX,钳制到边Clamp To Edge)OpenGL:在这种情况下,超出边界\([0,1]\)的值会被钳制到边界。因此会一直重复纹理图像边界处的值。它能避免采样时,取到相对的边上的值,比如当双线性插值发生在纹理边缘附近时。

  • 边界颜色Border)DirectX,钳制到边界颜色Clamp To Border)OpenGL:在这种情况下,当超出\([0,1]\)的纹理坐标时,会取另外一个被单独定义的边界颜色。这个函数对于渲染贴画到单颜色表面上来说很有用,例如,纹理的边缘会和边界颜色流畅地混合。

下图是不同环绕模式(纹理寻址模式)的例子

img

每个纹理坐标轴可以使用不同的对应函数,比如,纹理可以在u轴方向上重复,但是在v轴方向上被钳制。在DirectX中也有着单次镜像Mirror Once)模式,即镜像行为只会有一次,在这之后纹理坐标会被钳制,它对于对称贴花来说很有用。
  重复的纹理是开销不那么昂贵的添加视觉细节到场景中的一种方式。然而,在纹理重复大约三次后,整体会看起来不那么令人信服,因为眼睛会察觉出图案的存在。一个通常被用来避免这种周期性问题的方法是结合纹理值与其它非环绕的纹理。这个方法可以被大幅地扩展,就如同Andersson所描述的商业地形渲染系统那样。在这个系统中,多个纹理会基于地形的类型、海拔、坡度以及其它因素被结合到一起。纹理图像也会与几何模型例如灌木丛和岩石这种在场景中摆放的物体相关联。
  避免周期性的另一个选项是使用着色器程序来实现专门的对应函数,这种对应函数会随机地重组图案。王氏砖块Wang Tiles)是这种方法的一个例子。一个王氏砖块集是一个小的一系列边缘匹配的方形块的集合。方形块在纹理映射的过程中会被随机挑选。Lefebvre和Neyret实现了一个类似的对应函数,他们使用了依赖的纹理读取和表来避免图案重复。
  最后要介绍的被应用的对应函数是隐式的,它会取决于图像的大小。一个纹理通常不能直接使用uv坐标进行访问,而是要先乘以图像的分辨率,这样才能读取图像在某个位置存储的值。使用uv坐标的优势在于可以使用不同分辨率的图像纹理,而又不需要改变顶点存储的值。

纹理值(Texture Values)

  在使用对应函数得到了纹理空间的坐标后,这个坐标接着会被用来获取纹理值。对于图像纹理来说,这是通过访问纹理,从图像取回纹素信息做到的。在下一个部分,这个过程将被多次涉及。图像纹理映射是纹理在实时图形中被使用的一个主要途径,但是过程式函数也可以被使用。在过程式纹理映射中,从纹理空间获取一个纹理值并不涉及内存查找,而是通过函数计算出来的。它在后续的部分也会被讨论。
  最直接的纹理值是一个RGB值,它能被用来替换或修改表面的颜色。相似的,单独一个灰度值可以被返回。另一种返回的数据类型是RGB\(\alpha\),如上个章节提到的那样。阿尔法值通常是颜色的不透明度,它决定了颜色对像素有多大的影响。除了以上这些,另一种值类型也可以被存储,比如表面粗糙度。有非常多类型的数据可以被存储于图像纹理中,正如后续我们会看到的凹凸映射那样。
  从纹理返回的值在被使用前还进行变换。这些变换可以在着色器程序中进行。一个常见的例子是重新映射\([0.0,1.0]\)范围到\([-1.0,1.0]\)范围,这被用于存储于颜色纹理中的着色法线。

图像纹理映射(Image Texturing)

  在图像纹理映射中,一个二维图像会被高效地贴到一个或更多三角形组成的表面上。我们已经了解了纹理空间的位置是如何被计算的。现在我们将会专注于一些问题和使用纹理空间的位置从图像纹理读取纹理值的一些算法。这章的余下部分会简单地把图像纹理称为纹理Texture)。此外,当我们提到一个像素的单元格时,我们指代的是围绕像素的屏幕网格单元格。正如上个章节讨论的那样,一个像素Pixel)实际上是个被显示的颜色值,(为了更好的质量)它会被与之相关联的网格单元格之外的样本影响。
  在这个部分,我们会格外地专注于那些快速采样并滤波纹理图像的方法。上一章讨论了走样的问题,特别是渲染物体的边缘会遇到的走样问题。纹理也会有采样问题,在被渲染的三角形的内部会发生。
  像素着色器通过纹理坐标值和texture2D调用来访问纹理。这些值是uv纹理坐标,会被对应函数映射到\([0.0,1.0]\)的范围。GPU会负责将其转换到纹素坐标。在不同的API中,纹理坐标系统有两个主要的区别。在DirectX中,左上角落的坐标是\((0,0)\),右下角落的坐标是\((1,1)\)。这和很多图像类型存储数据的方式相匹配,因为在这些图像类型的文件中,顶行是最先被存储的。而在OpenGL中,左下角落是\((0,0)\),右上角落是\((1,1)\),和DirectX的区别是y轴被翻转了。纹素都有着整数坐标,但是我们通常想访问纹素之间的位置,混合这个位置周围的纹素。这带来了一个问题,我们首先得搞清楚像素中心的浮点坐标是多少。Heckbert讨论了两种可能的系统,一种为截断,另一种为舍入。DirectX 9使用了舍入系统让左上角的像素中心在\((0.0,0.0)\)。这个系统有时候会带来困惑,因为左上角落的坐标为\((-0.5,-0.5)\)。后来的DirectX 10改变到了OpenGL使用的截断系统,让最左上角的像素中心的坐标为\((0.5,0.5)\)。更确切地说,这个系统叫向下取整系统,在这之中小数部分会被抛弃。它是一个更自然的系统,能更好的映射到编程语言。比如,像素\((5,9)\)定义了5.0到6.0的u坐标范围和9.0到10.0的v坐标范围。
  有一个值得解释的术语是依赖的纹理读取Dependent Texture Read),它有着两个定义。第一个适用于移动式设备。当通过texture2D访问纹理时,一个依赖的纹理读取会发生,不论像素着色器是否计算纹理坐标,又或是使用未修改的来自顶点着色器传入的纹理坐标。更老的那些不支持OpenGL ES 3.0的GPU在没有依赖的纹理读取的情况下会运行地更高效,因为纹素数据可以被预取。另一个更老的定义对于早年的桌面端GPU来说非常重要。在这个上下文中,一个依赖的纹理读取会发生在一个纹理坐标依赖于一些之前的纹理值时。例如,一个纹理可能会改变着色法线,这导致了立方体贴图被访问的坐标的改变。这种功能在早期的GPU上是被限制或甚至是不存在的。在当下,这种读取对性能有影响,具体取决于一个批次中被计算的像素数量以及其它的因素。Section 23.8有更多的信息。
  在GPU中使用的纹理图像的维度通常为\(2^m \times 2^n\),其中的\(m\)\(n\)都是非负的整数。这些是二的幂次的Power-of-twoPOT)纹理。现代的GPU可以处理非二的幂次的Non-power-of-twoNPOT)任意尺寸的纹理。然而有些更老的移动式GPU可能不支持NPOT纹理的mipmapping。图形加速器一般有不同的上限纹理尺寸。DirectX 12允许最多\(16384^2\)个纹素。
  假设我们有一个有着\(256 \times 256\)纹素的纹理,想把它映射到正方形上。只要在屏幕上被投影的正方形的尺寸和纹理的尺寸差不多一致,那么在正方形上的纹理会和原始图像看起来差不多一样。但是,如果被投影的正方形在屏幕上覆盖的像素数量是原始图像像素数量的十倍呢?又或者,在屏幕上覆盖的像素数量是原始图像像素数量的十分之一呢?这两种情况实际上分别涉及放大Magnification)和缩小Minification),我们需要分别决定使用哪种采样和滤波方法。
  在这章讨论的图像采样和滤波方法会应用于从每个纹理读取的值。然而,我们的目的是想在最终被渲染的图像中避免走样,这在理论上要求对最终的像素颜色进行采样和滤波。区别就在于滤波着色方程的输入还是滤波着色方程的输出。只要输入和输出是线性相关的(这对于颜色来说成立),那么滤波单独的纹理值就等价于滤波最终的颜色。然而,许多存储于纹理中的着色器输入值,就比如表面法线和粗糙度值,它们和输出有着非线性的关系。标准的纹理滤波方法用于这些纹理来说会产生走样。用于这种纹理的改善的滤波方法会在Section 9.13被讨论。

放大(Magnification)

img

三张小图分别为:最近邻采样、双线性插值、三次卷积插值

  在上图中,一个有着\(48 \times 48\)纹素的纹理被映射到了一个正方形上,这个正方形被近距离进行了观察,底层的图形系统因此要放大纹理。最常用的用于放大的滤波技术有最近邻采样Nearest Neighbor)(实际上使用了一个方框滤波器)和双线性插值Bilinear Interpolation)。此外,还有三次卷积插值Cubic Convolution),它使用了\(4 \times 4\)\(5 \times 5\)的纹素加权后的和。这能有更高的放大质量。尽管原生的三次卷积插值的支持在当下的硬件中没有广泛地存在,但是它可以在着色器程序中手动实现。
  在上图的左小图中,最近邻采样被使用了。可以看到每个纹素变得显眼了起来。这个效果被称为像素化Pixelation),发生这种现象是因为放大的时候使用了最近的纹素,从而导致了一种块状的外表。虽然这个方法的质量很差,但是在每次采样的时候只会取一个纹素。
  中间的图像使用了双线性插值(有时被称为线性插值)。对于每个像素来说,这种类型的滤波会找到近处的四个相邻纹素,并分别在两个维度(u维度和v维度)上进行线性插值,最终为滤波计算出一个混合后的值。结果看起来会模糊一些,使用最近邻方法导致的锯齿感消失了大半。你可以做个小实验,眯眼观察左边的图像,这和使用低通滤波器的效果差不多一致。
  回到170页最开始的砖块纹理,如果不抛弃小数部分,我们会得到坐标\((p_x,p_v)=(81.92,74.24)\)。我们在这里使用OpenGL的左下原点纹素坐标系统,因为它和标准的笛卡尔坐标系统匹配。我们的目标是在四个最近的纹素之间插值,使用这几个纹素的中心可以定义一个纹素大小的坐标系统。如下图所示

img

为了找到最近的四个像素,我们让采样位置减去\((0.5,0.5)\),从而得到\((81.42,73.74)\)。抛弃小数部分,四个最近的像素的范围为\((x,y)=(81,73)\)\((x+1,y+1)=(82,74)\)。小数部分为\((0.42,0.74)\),我们记这个位置为\((u^\prime,v^\prime)\)。观察上图,我们可以知道它表示左下像素与采样位置的相对位置,换个方向来理解,这个位置的两个分量分别表示右上像素的水平权重和竖直权重。因此右上像素的权重为水平权重\(u^\prime\)与竖直权重\(v^\prime\)之积\(u^\prime v^\prime\),我们可以类推出剩余三个像素占的权重分别为\(u^\prime (1-v^\prime)\)\((1-u^\prime)v^\prime\)\((1-u^\prime)(1-v^\prime)\),因此双线性插值函数\(\mathbf{b}(p_u,p_v)\)的公式为

\[\begin{align*} \mathbf{b}(p_u,p_v) &= (1-u^\prime)(1-v^\prime)\mathbf{t}(x,y)\\ &+u^\prime(1-v^\prime)\mathbf{t}(x+1,y)\\ &+(1-u^\prime)v^\prime \mathbf{t}(x,y+1)\\ &+u^\prime v^\prime \mathbf{t}(x+1,y+1) \end{align*} \]

  从直觉上说,相对于采样位置更近的纹素对最终颜色的贡献会越大。这个公式确实有这个含义。
  一个常见的用来在放大时对抗模糊的方法是使用细节纹理Detail Texture)。这些是表示精细的表面细节的纹理,比如手机上的划痕或是广袤地面上的灌木丛。这样的细节会以不同的尺度作为另一张纹理被叠加到放大后的纹理上。细节纹理的高频重复图案与低频放大后的纹理结合会和使用了单独一个高分辨率纹理相似。
  双线性插值会在两个方向上进行线性插值。然而,线性插值在某些时候是不需要的。假设有个纹理是由黑白像素组成的棋盘图案。使用双线性插值会得到灰度逐渐变化的样本。假设进行一次重映射,所有灰度低于0.4的都变成黑色,所有灰度高于0.6的都变白色,在这之间的会填充黑白之间的间隙,纹理会看起来又像棋盘,同时也会有一些混合后的颜色。下图是个相关的例子

img

  使用一个高分辨率纹理会有类似的效果。例如,想象一下每个棋盘格子是由\(4 \times 4\)而不是\(1 \times 1\)的纹素构成的。在这种情况下,格子中央的周围区域的插值将会是纯黑或纯白。
  最开始的那张人像图的右小图使用了一个双三次滤波器,残余的块状感已经差不多没了。这里需要注意双三次滤波器比双线性滤波器要昂贵。然而,许多高次的滤波器可以使用重复的线性插值来表示。因此,GPU硬件提供的线性插值可以被利用。
  如果你认为双三次滤波器太昂贵了,Qu´ılez提供了一个简单的技术,这个技术使用了一个光滑的曲线,在\(2 \times 2\)的纹素之间进行插值。我们首先描述曲线接着再描述这个技术。两种被常用的曲线为smoothstep曲线和quintic曲线,它们的公式还有函数图如下所示

\[\begin{align*} s(x) &= x^2(3-2x) \;\;\;\; \text{smoothstep} \\ q(x) &= x^3(6x^2-15x+10) \;\;\;\; \text{quintic} \end{align*} \]

img

这两个对于光滑插值来说很有用,可能是你想用到的。smoothstep曲线有着\(s^\prime(0)=s^\prime(1)=0\)的性质,而且在0到1之间是光滑的。quintic曲线和smoothstep有着相同的性质,不过它还有\(q^{\prime\prime}(0)=q^{\prime\prime}(1)=0\)的性质,也就是二阶导在曲线起始处和终止处都为\(0\)
  这个技术开始于计算\((u^\prime,v^\prime)\),首先把纹理坐标与纹理维度相乘,然后在加上\(0.5\)。接着,整数部分会被保留用于后续的运算,小数部分会作为\((u^\prime,v^\prime)\),两个分量都会在\([0,1]\)范围内。\((u^\prime,v^\prime)\)坐标接着会使用\((q(u^\prime),q(v^\prime))\)被变换到\((t_u,t_v)\),范围还是会在\([0,1]\)。变换后的坐标接着会减去\(0.5\),且整数部分会被加回来。这样运算后的u坐标接着除以纹理宽度,v坐标也做一样的处理除以纹理高度。这个时候,新的纹理空间的坐标接着可以被用来进行双线性插值。要注意的是,这个方法会给予每个像素平台,如果纹素位于RGB空间的一个平面上,使用这种类型的插值会得到一个光滑的但是像阶梯的曲线,在一些情况下可能不让人满意。下图是一张示例图

img

注解:最近邻、线性、quintic曲线、三次插值

缩小(Minification)

img

  当纹理被缩小了,数个纹素可能会覆盖同一个像素单元格,正如上图所示。为了给每个像素一个正确的颜色,我们应该综合考量不同纹素对同一像素的影响。然而,精确做到这件事是很困难的,在实时图形中就更困难了。
  因为有了这个限制,GPU使用了一些方法来应对这一点。一个方法是使用最近邻采样,它和放大部分中讲述的最近邻采样完全一致,即读取最近的纹素。然而这会导致严重的走样问题。在下图中,最上方的小图使用了最近邻采样。

img

随着距离越来越远,走样开始出现,这是因为在远处会有多个纹素影响着同一个像素,只取一个纹素当然会造成走样。当观察者和表面发生相对移动时,这种伪影会变得更加显眼,它被称为时间走样Temporal Aliasing)。
  另一种可用的滤波器为双线性插值,也和之前描述的一样。对于缩小来说,它比最近邻采样好了些许。它会混合四个纹素,而不是只使用一个纹素。但是当像素被四个以上的纹素影响时,双线性插值会失败,导致走样的产生。
  更好的方法是可能的。走样可以通过一些采样和滤波技术来缓解。纹理的信号频率会取决于纹理的纹素在屏幕上的密集程度。根据奈奎斯特极限,我们需要确保纹理的信号频率不超过采样频率的一半。例如,假设一张图像由黑白线交替构成,每条线间隔一纹素。那么波长是两个纹素宽,因此频率是\(\frac{1}{2}\)。为了恰当地在屏幕上显示这个纹理,采样频率至少得是\(2 \times \frac{1}{2} = 1\),也就是说每纹素都得被采样。因此,每个纹素至少被采样一次。
  为了达到这个目标,要不像素的采样频率得提升,要不纹理的频率得下降。上一个章节讨论的抗走样方法提供了许多提高像素采样率的途径。然而这些提升是有限度的。为了更全面地解决这个问题,我们需要使用不同的图像缩小算法。
  所有的纹理抗走样算法的基本想法都是一样的,即预处理纹理并创建能帮助计算一系列纹素对同一像素影响的近似估计的数据结构。对于实时图形来说,这些算法有着使用固定量的时间和固定量的资源用于执行的特点。使用这些算法,每像素可以取固定量的样本并把它们结合到一起,来计算潜在大量的纹素对像素的影响。

Mipmapping

  最受欢迎的纹理抗走样方法被称为mipmapping。当前所生产的所有图形加速器都会以一些形式实现它。“Mip”表示multum in parvo,这是拉丁语,它的意思是“在一个小地方的许多东西”,它对于从原始纹理开始一级一级地滤波到更小的图像来说是个好名字。
  当mipmapping缩小滤波器被使用时,在真正的渲染发生前,原始的纹理会被补充一些它的更小尺寸的版本。第零级也就是原始纹理会被降采样到一张四分之一面积的纹理上,每个新纹素值是原始纹理上四个相邻纹素的平均。新的一级也就是第一级在有些时候被称为原始纹理的子纹理Subtexture)。这个过程可以一直进行下去,下方为一张形象的示例图。这一系列的图像通常被称为mipmap链

img

  获得高质量的mipmap有两个重要的地方,第一是要有好的滤波,第二是要有伽马校正。一个常用的滤波方法是取一个mipmap的\(2 \times 2\)的纹素,对它们进行平均来获得下一个mipmap的纹素值。进行平均使用的滤波器可以是方框滤波器,不过它会导致差的质量,因为它会不必要地模糊低频率,而且同时保留一些造成走样的高频率。更好的方法是使用Gaussian、Lanczos、Kaiser或类似的滤波器,对于mipmapping这个任务来说已经有了一些快速且开源的代码,而且有些API支持在GPU上使用更好的滤波。在纹理边缘处的滤波必须要小心处理,因为要考虑到具体使用的环绕模式(纹理寻址模式)。
  对于在非线性空间编码的纹理来说(比如大多数颜色纹理),在滤波时忽略伽马校正会修改mipmap的感知亮度。当你距离物体越来越远时,不正确的mipmap会被使用,整个物体看起来会更暗,对比度和细节因此会受到影响。正是因为这样,所以应该先把这种纹理从sRGB转换到线性空间,接着在线性空间进行mipmap滤波,最后把结果转换回sRGB颜色进行存储。大多数API都支持sRGB纹理,而且会在线性空间生成mipmap并把最终的结果转换回sRGB进行存储。当sRGB纹理被访问的时候,它们的值会先被转换到线性空间,来让放大和缩小正确执行。
  之前提到过,有些纹理与最终被着色的颜色有着非线性关系。使用常规的滤波会有问题,而mipmap的生成对这个问题格外敏感,因为会有成百上千的像素被滤波。为了达到最好的效果,对于这种纹理来说应该使用专门的mipmap生成方法。这些方法会在Section 9.13中被详细解释。

img

  进行纹理映射时,mipmap结构的访问过程其实很直接。一个屏幕上的像素会占据纹理上的部分区域。当像素在屏幕上的范围被投影到纹理上时,它有可能包含一个或多个纹素,上方为一张相关的示例图。使用像素单元格的边界不是严格正确的,但是我们在这里使用它来简化表示。在像素单元格之外的纹素也能影响像素的颜色。我们的目标是粗略估计纹理对像素的影响。有两种常用的方法来计算\(d\)(在OpenGL中被称为\(\lambda\),它也被称为纹理细节等级Texture Level Of Detail))。第一个方法会使用由像素单元格形成的四边形的长边来近似计算像素的覆盖范围。另一个方法会使用四个微分\(\partial u/\partial x\)\(\partial v/\partial x\)\(\partial u/\partial y\)\(\partial v/\partial y\)绝对值中最大的那一个来进行计算。这几个微分是纹理坐标在屏幕坐标轴上的变化速度。Williams的原始文章和Flavell或Pharr的文章有着更多关于这些公式的信息。McCormack等人讨论了最大绝对值方法导致的伪影,并提出了一个替代公式。Ewins等人分析了一些算法在差不多质量情况下的硬件开销。
  使用Shader Model 3.0或更新版本,像素着色器程序可以访问这些梯度值。因为它们都基于相邻像素之间的差异,因此在动态流控制的影响下,像素着色器会无法进行访问。如果纹理读取要在这种部分(例如循环)中进行,那么导数必须要先一步被计算出来。要注意的是,顶点着色器不能访问梯度信息,因此在使用顶点纹理映射时,梯度值或细节等级需要在顶点着色器中进行计算并提供给GPU。
  计算出坐标\(d\)后,我们就知道要在mipmap金字塔轴线的哪个位置进行采样。我们的目标是每个纹素至少被采样一次,来达到奈奎斯特率。当像素单元格覆盖了更多的纹素时\(d\)会变得更大,更模糊版本的纹理会被访问。\((u,v,d)\)三元组会被用来访问mipmap。其中的\(d\)就好比于纹理等级,但是除了整数值,它还有小数部分代表着与等级之间的距离。因此,在\(d\)之下和之上的等级都得被采样。\((u,v)\)位置会被使用来获取在两个等级上进行双线性插值后的结果,双线性插值后的两个结果接着又会被线性插值,这取决于每个纹理等级到\(d\)的距离。整个过程就被称为三线性插值Trilinear Interpolation),它会逐像素进行。
  用户一般能控制的是细节等级偏移Level Of Detail Bias)。\(d\)会与它相加,因此能影响纹理的感知锐度。如果我们增加\(d\)向金字塔的上方移动,最终看到的纹理会看起来更模糊一些。如果要保持好的视觉质量,不同类型的纹理会使用不同的LOD偏移,LOD偏移也可能有不同的使用方式。例如,如果图像显得模糊了,那么可以使用负的偏移。对于滤波不佳的(走样的)合成图像,如果要进行纹理映射,那么可以使用正的偏移。此外,在采样时可以让每个像素使用相同的偏移,又或是逐像素在像素着色器中声明。对于更精细的控制来说,\(d\)坐标或用于计算的导数可以由用户提供。
  mipmapping的优势在于与其逐个计算纹素对某个像素的影响,不如访问并插值被预结合的一系列纹素。这个过程会花费固定量的时间,它和缩小的程度无关。然而,mipmapping有着一些缺陷。其中一个主要的缺陷是过度模糊Overblurring)。假如有个像素单元格在纹理的u轴方向上覆盖了大量的纹素,但是在v轴方向上只覆盖了些许纹素。这种情况会发生在观察者以几乎侧向倾斜的视角沿着纹理映射后的表面的某个方向观察的时候。这种情况实际要求在一个轴上进行放大处理,而在另一个轴上进行缩小处理。但是访问mipmap的效果是取回纹理上的正方形区域。为了避免走样,最简单的方法是选择像素单元格的近似覆盖范围的最大估计。这就导致了取回的样本看起来会相对模糊一些。下方为一张相关的示例图

img

累积面积表(Summed-Area Table)

  另一种用来避免过度模糊的方法是累计面积表Summed-area TableSAT)。为了使用这个方法,首先得创建一个和纹理相同尺寸但有着更多比特精度的数组(比如每通道16比特位)。在这个数组的每个位置,必须计算并存储从原点\((0,0)\)纹素到这个位置所在的纹素形成的矩形内的所有纹素的和。在纹理映射的时候,像素单元格投影到纹理上的范围可以被一个矩形包围。累计面积表接着会被访问来决定这个矩形的平均颜色,这个平均颜色会被传递返回作为纹理在那个像素的颜色。平均颜色会使用纹理坐标进行计算,如下图所示。

img

公式为

\[\mathbf{c} = \frac{\mathbf{s}[x_{ur},y_{ur}]-\mathbf{s}[x_{ur},y_{ll}]-\mathbf{s}[x_{ll},y_{ur}]+\mathbf{s}[x_{ll},y_{ll}]}{(x_{ur}-x_{ll})(y_{ur}-y_{ll})} \]

这个公式其实很好理解,原点到\((x_{ur},y_{ur})\)的总和减去原点到\((x_{ur},y_{ll})\)的总和再减去原点到\((x_{ll},y_{ur})\)的总和,由于多减了一份从原点到\((x_{ll},y_{ll})\)的总和,所以得补偿回来。补偿后的结果再除以\((x_{ur}-x_{ll})(y_{ur}-y_{ll})\)就得到了包围矩形的平均颜色。
  使用累计面积表的一张示例图如下方所示

img

可以看到右边的伸向地平线的直线更锐利些了,但是对角线方向上还是过度模糊。问题在于当沿着纹理的对角线观察纹理时,一个大的包围矩形会被生成,相比于使用其它方向观察生成的包围矩形来说,它会包含很多不在像素覆盖范围的纹素,因此对角线方向上会有过度模糊。
  累积面积表是各向异性滤波Anisotropic Filtering)算法中的一种。这种算法会取回非正方形范围的纹素值。然而,SAT主要在水平和竖直方向上最高效。此外也要注意到累计面积表对于\(16 \times 16\)或更小尺寸的纹理来说有着至少两倍的内存开销,对于那些更大的纹理来说则会需要更高的精度。
  累计面积表在提高质量的同时有着相对合理的总体内存开销,它可以在现代的GPU上进行实现。改善的滤波对于高级渲染技术的质量来说极其重要。例如,Hensly等人提供了一个高效的实现并展示了累计面积采样是如何提升镜面反射的。另外一些用到区域采样的算法也可以被SAT改善,就比如景深(Depth Of Field)、阴影贴图(Shadow Map)、模糊反射(Blurry Reflection)。

无约束各向异性滤波(Unconstrained Anisotropic Filtering)

  在当下的图形硬件中,最常用的用来进一步提升纹理滤波的方法是重复利用现存的mipmap硬件。它的基本想法是像素单元格会被反向投影到纹理上的四边形,在这个纹理上的四边形内部会有多次采样,这些样本会被结合起来。正如之前提到的那样,每个mipmap的样本都有着一个位置和一个与之关联的方形区域。与其使用单个mipmap的样本来近似四边形的覆盖范围,这个算法使用了一些正方形来覆盖四边形。四边形最短的那个边被用来决定\(d\)。对于每个mipmap样本来说,这会让求平均的面积更小(同时更不那么模糊)。而四边形的长边会被用来创建一条和长边平行的从四边形中间穿过的各向异性线Line Of Anisotropy)。当各向异性在\(1:1\)\(2:1\)之间时,会有两个样本取于这根线上,如下图所示。更高比例的各向异性会需要在这根线上取更多的样本。

img

  这个方法让各向异性线可以有任何方向,也没有累计面积表那样的限制。相比于mipmap来说也不需要更多的纹理内存,因为它使用mipmap算法用于采样。一个使用各向异性滤波的例子如下图所示。

img

  这个算法由Schilling等人提出,并由他们的Texram动态内存设备实现。后续有一些工作对这个算法进行了实现。他们用到的算法和椭圆加权平均Elliptical Weighted AverageEWA)滤波器这种软件采样算法在质量上相近,它会把像素的影响范围变换到纹理上的一个椭圆,并使用一个滤波核(Filter Kernel)为椭圆内的纹素加权。Mavridis和Papaioannou展示了在GPU上使用着色器代码实现EWA滤波的一些方法。

体积纹理(Volume Textures)

  图像纹理的扩展是可以通过\((u,v,w)\)访问的三维图像数据。例如,医学成像数据可以作为一个三维网格被生成。通过移动一个多边形穿过网格,可以看到这些数据的二维切片。一个相关的想法是用这种形式来表示体积光照。表面上某点的光照会由三维体内对应位置上的值决定,这个值会与光照方向结合。
  绝大多数GPU都支持体积纹理的mipmapping。因为在一个体积纹理中的某个mipmap等级的滤波涉及三线性插值,因此在不同的mipmap级别之间的滤波需要四线性插值Quadrilinear Interpolation)。因为这涉及16个纹素的平均,所以精度问题有可能发生,这可以通过使用更高精度的体积纹理来解决。Sigg和Hadwiger描述了这个问题还有其它和体积纹理相关的问题,并提供了一些高效的方法来进行滤波和其它的操作。
  尽管体积纹理有着显著更高的存储开销以及更高的滤波开销,但是它们有着一些独特的优势。找到三维网格好的二维参数化形式的复杂过程可以被跳过,因为三维位置可以直接被用于纹理坐标。这避免了在二维参数化中通常会遇到的畸变和缝隙问题。体积纹理也可以被用来表示材质的体积结构,就比如木头和大理石。一个使用了这种纹理的模型将会表现得像从这种材质雕刻而成。
  对于表面的纹理映射来说,使用体积纹理是极其低效的,因为有很多样本没有被使用。Benson和Davis还有DeBry等人讨论了在稀疏八叉树结构中存储纹理数据的方法。这个方法很适用于实时的三维绘画系统,因为表面在被创建的时候是不需要被显式分配纹理坐标的,而且八叉树可以容纳任何期望的细节等级的纹理细节。Lefebvre等人讨论了八叉树纹理在现代GPU上进行实现的细节。Lefebvre和Hoppe讨论了压缩稀疏三维体的数据到一个显著更小的纹理的方法。

立方体贴图(Cube Maps)

  另一种纹理类型是立方体纹理Cube Texture)或立方体贴图Cube Map),它有着六个正方形纹理,每个纹理与立方体的一个面相关联。一个立方体贴图需要一个从立方体中心指向外部的向量来访问。向量中数值最大的那一个分量会决定采样立方体贴图的哪个纹理,比如\((-3.2,5.1,-8.4)\)就决定了\(-z\)面会被访问,同理可以得到其它的面的选择情况。剩余的两个分量则会决定纹理坐标是多少,它们会除以数值最大分量的绝对值,接着被简单地映射到\([0,1]\)范围。例如对于之前的例子来说会有\(((-3.2/8.4+1)/2,(5.1/8.4+1)/2) \approx (0.31,0.80)\)。立方体贴图对于表示方向的函数在各个方向上的数值来说很有用,它们是最常被用于环境映射的。

纹理表示(Texture Representation)

  对于处理一个应用程序中的多个纹理来说有一些改善性能的方法。纹理压缩会在这章后续的部分被讨论,这个部分的焦点在于纹理图集(Texture Atlases)、纹理数组、无绑定纹理(Bindless Texture),它们的目的都在于避免渲染时改变纹理带来的开销。在Section 19.10.1和19.10.2,纹理串流和转码会被描述。
  为了让GPU批次处理尽可能多的工作,尽可能少的改变状态是一直被提倡的。为了尽可能这么做,一些图像可能被放置到单独一个更大的纹理中,这个纹理被称为纹理图集Texture Atlas),它的一个例子如下图所示。

img

这里要注意,子纹理的形状可以是任意的,下图是个相关的例子。

img

Nöll和Stricker描述了子纹理在图集中的摆放的优化。此外,进行mipmap生成和访问时需要小心,因为更高级别的mipmap可能包含一些分开的不相关的形状。Manson和Schaefer展示了一个利用表面的参数化来优化mipmap的创建的方法,它能生成好得多的结果。Burley和Lacewell展示了一个叫Ptex的系统,在这个系统中的每个细分表面的四边形都有着属于它的一个小纹理。它的优势在于能避免在网格上分配独特的纹理坐标,而且在纹理图集断开的部分的缝隙上没有伪影。为了更好的在四边形之间进行滤波,Ptex使用了一个邻接数据结构。虽然最初的目标是离线渲染,Hillesland展示了打包PtexPacked Ptex),它会把每个面的子纹理放到一个纹理图集中,并使用来自相邻面的填充来避免滤波时的间接访问。Yuksel展示了网格颜色纹理Mesh Color Textures),它是在Ptex之上的改进。Toth为Ptex类似的系统提供了高质量的面上滤波方法,超出\([0,1]^2\)范围的滤波抽头会被抛弃。
  对于纹理图集来说,使用环绕或重复还是镜像模式会有难度,因为这些模式得作用于子纹理而不是纹理图集。当生成图集的mipmap时,另一个问题有可能发生,一个子纹理有可能泄露到另一个子纹理上。在放置子纹理到一个更大的纹理图集前,可以通过为每个子纹理分开生成mipmap层次结构并为子纹理使用二的幂次的分辨率来避免这个问题。
  一个更简单的解决方法是使用API来创建纹理数组Texture Array),它能完全避免mipmapping和重复模式会遇到的问题。它的一个例子如下图所示

img

在纹理数组中的所有子纹理都必须有相同的维度、格式、mipmap层级结构、MSAA设置。和纹理图集一样,纹理数组的创建只需要一次,数组的任意元素接着可以通过索引在着色器中进行访问。这比绑定每个子纹理要快上五倍。
  还有一个叫无绑定纹理Bindless Texture)的特性可以帮助避免状态改变。在没有无绑定纹理的帮助下,一个纹理需要使用API被绑定到某个特定的纹理单元。然而,纹理单元的数量是有上限的,这对于编程者来说是个要关注的问题。驱动会保证纹理驻留在GPU一端。有了无绑定纹理,纹理的绑定就没有了上限,因为每个纹理仅与一个64位指针关联,它有时候被称为(作为纹理的数据结构的)句柄Handle)。这些句柄可以使用许多方式来访问,比如uniform、varying数据、来自其它纹理的数据、着色器存储缓冲对象(SSBO)。应用程序需要确保纹理驻留在GPU一端。无绑定纹理避免了驱动中任意类型的绑定开销,因此让渲染变得更快了。

纹理压缩(Texture Compression)

  一个用来应对内存和带宽问题以及缓存忧虑的方法是固定比率的纹理压缩Texture Compression)。通过让GPU在运行时解压纹理,纹理可以使用更小的纹理内存来存储,这增加了缓存的有效大小。另一个重要的一点是,这种纹理使用起来更高效,因为它们在被访问时有更低的内存带宽开销。另一个相关的但是不同的用法是通过压缩来使用更大的纹理。例如,一个分辨率为\(512^2\)的每纹素3字节的未压缩纹理会占据\(768\)千字的存储空间。如果使用压缩率为6:1的纹理压缩,那么一个\(1024^2\)分辨率的纹理只会占用512千字的存储空间。
  JPEG和PNG等不同的图像格式使用了各种各样的图像压缩方法,但是在硬件中实现相应的解压算法的开销会很高。S3开发了一个叫S3纹理压缩S3 Texture CompressionS3TC)的方法,它对于DirectX来说是标准方法,被其称为DXTC,而在DirectX 10中它被称为区块压缩Block CompressionBC)。此外,它是OpenGL的事实标准,因为几乎所有的GPU都支持它。它有着以固定的大小创建压缩图像的优势,此外压缩后的纹理会有独立的编码片段,解码起来会更加快速。图像的每个压缩部分的解压不需要考虑别的压缩部分。此外也不需要共享的查找表或其它的依赖项,从而使解码简化了。
  DXTC/BC压缩方法有七个变体,它们有着一些共同点。编码是在\(4 \times 4\)的纹素区块之上进行的,纹素区块也被称为Tile)。每个区块是分开进行编码的。编码会基于插值进行。对于每个编码后的量来说,两个参考值会被存储。区块中的每个纹素会有一个插值因子。它会挑选在两个参考值之间的线段上的一个值,这个值可以等于参考值或从参考值插值得到。压缩来自仅存储两个颜色和每个像素的短的索引值。
  实际使用的编码在七种变体中会有所区别,下表进行了相关总结。

img

注解:这些编码都压缩\(4 \times 4\)的纹素区块。表中的“B”是字节(Byte),而bpt为每纹素的比特位。

要注意的是“DXT”指代DirectX 9,而“BC”是在DirectX 10及后续的版本所用的名称。正如表中看到的那样,BC1有着两个16比特位的参考RGB值(5比特位的红通道,6比特位的绿通道,5比特位的蓝通道),而且每个纹素都有着一个2比特位的插值因子,用来在两个参考值或是两个中间值中选择。相对于未压缩的每纹素24比特位的RGB纹理来说有着6:1的压缩率。BC2使用了和BC1一样的颜色编码,但是为每纹素增加了4比特位用于量化后的原始阿尔法。对于BC3来说,每个区块的RGB数据的编码方式会和DXT1中每个区块的编码方式一样。此外,阿尔法数据会使用两个8比特位的参考值和每纹素3比特位的插值因子来编码。每个纹素可以在两个参考阿尔法值或是六个中间值中进行选择。BC4只有一个通道,会和BC3中的阿尔法有相同的编码。BC5包含两个通道,这两个通道的编码也和BC3的一样。
  BC6H是用于高动态范围(HDR)纹理的,这种纹理中的每个通道的值是使用16比特位的浮点值进行存储的。而BC6H这个模式使用了16字节,因此每纹素有着可用的8个比特位。它有着一个模式用于一条直线和另外一个模式用于两条直线,后一个模式中每个区块可以从一个小的分区集合中进行选择。两个参考颜色也可以被差分编码(delta-encoded)来获得更高的精度,而且可以根据使用的模式有着不同的精度。在BC7中,每个区块能有一到三条直线,而且每纹素可以有8个比特位。它的目标是8比特位RGB和8比特位RGBA纹理的高质量压缩。它和BC6H有着很多共性,但是被用于LDR纹理,而BC6H是用于HDR纹理的。要注意的是BC6H和BC7被称为BPTC_FLOAT和BPTC,在OpenGL中也一样。这些压缩技术可以被用于立方体纹理或是体积纹理还有二维纹理。
  这些压缩方法的主要缺点是有损的Lossy)。也就是说使用压缩后的数据进行重建是无法还原原始图像的。在BC1到BC5中,只有4或8个插值后的值会被用来表示16个像素。如果一个区块有着大量的相互之间有较大差异的值,那么压缩后会有些损失。在实践中,这些方案如果被恰当的使用,那么还是会有可以接受的图像保真度的。
  BC1到BC5的问题是被区块使用的颜色会处于RGB空间的一条直线上。例如,红色、绿色、蓝色是不能在单独一个区块中出现的。BC6H和BC7支持更多的直线,因此能提供更高的质量。
  对于OpenGL ES来说,另一个压缩算法被称为爱立信纹理压缩Ericsson Texture CompressionETC),这个算法被包含在了API中。这个方法有着和S3TC一样的特点,支持快速解码、随机访问、无间接查找、固定压缩率。它会把\(4 \times 4\)区块的纹素编码到64比特位的数据,每个纹素有可用的4个比特位。它的基本想法由下图所示

img

\(2 \times 4\)的区块会存储一个基础颜色。每个区块能从一个小的静态查找表中选择四个常量,而每个区块中的纹素可以选择表中的一个值进行增加。这会修改每个像素的亮度。图像的质量会与DXTC相当。
  在OpenGL ES 3.0包含的ETC2中,未被使用的比特组合会被用来为原始的ETC算法增加更多的模式。在BC1中,把参考颜色设置成相同的是无用的,这会导致一个有常量颜色的区块。而在ETC中,一个颜色也可以通过带符号的数从另一个颜色进行差分编码得到,又因为计算可以溢出和下溢。因此这种情况能被用来表示其它的压缩模式。ETC2增加了两个有着四个颜色的新模式,每个模式会以不同的方式为每个区块计算四个颜色,还有一个模式会使用RGB空间中的一个平面用于光滑渐变。爱立信阿尔法压缩Ericsson Alpha CompressionEAC)会压缩只有一个通道的图像。这个压缩和基础的ETC压缩相像,但是只对一个通道进行,压缩后的图像的每个纹素会使用4比特位。它可以选择性地与ETC2结合,此外两个EAC通道能被用来压缩法线。ETC1、ETC2和EAC都是OpenGL 4.0核心模式、OpenGL ES 3.0、Vulkan、Metal的一部分。
  法线贴图的压缩是值得注意的,被设计用于压缩RGB颜色的格式对于法线的压缩来说一般都不能正常工作。大多数方法都会利用法线都是单位长度的事实,且更进一步假设z分量是正的(对于切线空间的法线来说很合理)。这就使得仅仅需要存储法线的x分量和y分量。z分量是用如下的公式计算出的

\[n_z = \sqrt{1-n_x^2-n_y^2} \]

因为大多数GPU是原生不支持三通道纹理的,上述做法能避免潜在的通道浪费。更进一步的压缩一般可以通过使用BC5/3Dc格式的纹理做到。
  对于不支持BC5/3Dc或EAC格式的硬件来说,一个常用的替代方案是使用DXT5格式的纹理,在绿色和阿尔法通道中分别存储两个分量,剩余的两个通道不会被使用。
  PVRTC是一个纹理压缩格式,它被用于Imagination Technology的叫做PowerVR的硬件,而且同时也被iPhone和iPad广泛使用。它同时为每纹素2比特位和4比特位,以及\(4 \times 4\)大小的压缩区块提供了一个方法。它的核心想法是提供图像的两个低频信号,低频信号会通过使用相邻区块的纹素数据并进行插值得到。接着每纹素有1或2比特位用于在两个信号之间进行插值。
  自适应可伸缩纹理压缩Adaptive Scalable Texture CompressionASTC)不同在能把\(n \times m\)的纹素区块压缩到128比特位。区块的大小可以从\(4 \times 4\)一直到\(12 \times 12\),这导致了不同的压缩率,从每纹素0.89比特位开始一直到每纹素8比特位。ASTC使用了不同的技巧用于精简索引表示,每个区块可以选择直线的数量和端点编码。此外,ASTC可以处理有1至4个通道的纹理,且不论是LDR的纹理还是HDR的纹理。ASTC被包含在了OpenGL ES 3.2以及后续的版本中。
  上述讨论的所有的纹理压缩方法都是有损的,而且使用这些方法进行压缩所需的时间各有区别。压缩可以花费几秒,或甚至几分钟用于改善解码后的质量。因此,压缩一般会以离线预处理的形式完成,并被保存下来用于后续的使用。相对地,压缩也可以花费毫秒级的时间完成,代价是会有更低的质量,但是压缩后的纹理可以非常快地被解压出来并立即被使用。一个相关的例子是天空盒,它需要以每秒一次的频率进行更新,来让云层有移动。而解压是极其快速的,因为它会被固定功能的硬件完成。这个区别被称为数据压缩非对称性Data Compression Asymmetry),因为压缩相比于解压会花费相当长的时间。
  Kaplanyan提出了一些能改善压缩后的纹理的质量的方法。对于包含颜色和法线的纹理来说,贴图的每通道位宽至少得有16比特位。对于颜色纹理来说,可以执行一次直方图重归一化Histogram Renormalization),在着色器中使用一次缩放和偏移常量可以反转直方图重归一化的效果。直方图重归一化可以让图像存储的数值充分利用图像所使用的格式的整个可表示范围,这是一种高效的对比度增强。为每个通道使用16个比特位能确保直方图的每个槽位在重归一化之后都被使用,这能减少许多纹理压缩方法会产生的色带伪影。下方这张图为一个相关的例子

img

注解:从左至右为原始纹理、使用DXT1压缩的每通道8比特位的纹理、使用DXT1压缩的每通道16比特位的纹理

此外,Kaplanyan建议如果\(75\%\)的像素在\(116/255\)之上的时候,可以使用线性颜色空间,否则使用sRGB存储纹理。对于法线贴图,他也指出BC5/3Dc通常独立地压缩\(x\)分量,这意味着最优的法线并不总是能被找到的。它提出了使用以下的误差度量用于法线

\[e = \arccos(\frac{\mathbf{n} \cdot \mathbf{n}_c}{||\mathbf{n}||||\mathbf{n}_c||}) \]

其中的\(\mathbf{n}\)为原始的法线,而\(\mathbf{n}_c\)是从压缩后的法线解压出的法线。
  此外,还要注意的是纹理的压缩可以在不同的颜色空间中进行,这能用来加速纹理的压缩。一个常用的变换为\(\mathrm{RGB} \rightarrow \mathrm{YCoCg}\),它的公式如下所示

\[\begin{pmatrix} Y \\ C_o \\ C_g \end{pmatrix} = \begin{pmatrix} 1/4 & 1/2 & 1/4 \\ 1/2 & 0 & -1/2 \\ -1/4 & 1/2 & -1/4 \end{pmatrix} \begin{pmatrix} R \\ G \\ B \end{pmatrix} \]

其中的\(Y\)表示亮度,而\(C_o\)\(C_g\)表示色度。这个变换的逆如下所示

\[G = (Y + C_g),\;\; t=(Y-C_g),\;\; R=(t+C_o),\;\; B=t-C_o \]

可以看到只是一些加减法。因此这两个变换都是线性的,这是非常重要的一点,因为它能让纹理存储\(\mathrm{YCoCg}\)空间的值,且执行纹理映射的硬件可以在\(\mathrm{YCoCg}\)空间进行滤波,像素着色器可以把滤波后的值转换回\(\mathrm{RGB}\)空间。值得注意的是,这个变换本身是有损的,在一些某应用场景下要小心。
  另一个可逆的\(\mathrm{RGB} \rightarrow \mathrm{YCoCg}\)变换如下所示

\[\begin{cases} C_o&= R-B \\ t&=B+(C_o \gg 1) \\ C_g&=G-t \\ Y&=t+(C_g \gg 1) \end{cases} \Longleftrightarrow \begin{cases} t&=Y-(C_g \gg 1) \\ G&=C_g+t \\ B&=t-(C_o \gg 1) \\ R&=B+C_o \end{cases} \]

式中的\(\gg\)表示移位运算。这个式子意味着可以在两个方向上进行无损变换。如果\(\mathrm{RGB}\)的每个分量有着n比特位,那么\(C_o\)\(C_g\)应该有\(n+1\)的比特位来确保变换可逆。\(Y\)只需要\(n\)个比特位。Van Waveren和Casta˜no使用了有损的\(\mathrm{YCoCg}\)变换来在CPU或GPU上实现快速的DXT5/BC3压缩。他们让\(Y\)存储在阿尔法通道,让\(C_o\)\(C_g\)存储在\(\mathrm{RGB}\)的前两个分量中。压缩会很快因为\(Y\)是分开进行存储和压缩的。而对于\(C_o\)\(C_g\)分量,它们找到了一个二维的包围盒,并选择了包围盒的对角线来获得最好的结果。值得一提的是,对于那些在CPU上动态创建的纹理来说,最好一并在CPU上进行压缩。当纹理在GPU上创建时,最好也一并在GPU上被压缩。\(\mathrm{YCoCg}\)变换和其它的亮度-色度的变换通常被图像压缩所使用,在这些方法中色度分量通常在\(2 \times 2\)的像素上进行平均求得。这减少了\(50\%\)的存储需求,而且影响一般不大,因为色度的变化通常都比较慢。Lee-Steere和Harmon进一步把颜色转换到了色相-饱和度-明度(hue-saturation-value,HSV)空间,并在x轴和y轴方向上以4倍的缩放因子降采样色相和饱和度,并把明度存储于DXT1纹理中。Van Waveren和Casta˜no也提出了用于法线压缩的快速方法。
  Griffin和Olano的研究表明当一些纹理被映射到几何模型上,并使用复杂的着色模型进行着色时,这些纹理可以有较低的质量,因为并不会带来感知上的差异。因此,取决于使用场景,纹理质量的降低也许是可以接受的。Fauconneau展示了DirectX 11纹理压缩的一个SIMD的实现。

posted @ 2026-03-18 15:50  TiredInkRaven  阅读(3)  评论(0)    收藏  举报