表面的纹理就是它的外观和感觉——就像一幅油画的纹理。在计算机图形学中,纹理处理是一种使用图像、函数或其他数据源获取一个表面并在每个位置修改其外观的过程。例如,不是精确地表示砖墙的几何形状,而是将砖墙的彩色图像应用于由两个三角形组成的矩形。当矩形被查看时,彩色图像出现在矩形的位置。除非观者靠近墙壁,否则缺乏几何细节的问题将不会被注意到。
然而,一些有纹理的砖墙可能是不令人信服的原因,而不是缺乏几何形状。例如,如果砂浆是哑光的,而砖是光滑的,那么观察者会注意到两种材料的粗糙度是相同的。为了产生更真实的感觉,可以在表面应用第二个图像纹理。这种纹理不会改变表面的颜色,而是根据表面的位置改变墙壁的粗糙度。现在砖块和砂浆有了来自图像纹理的颜色和来自这个新纹理的粗糙度值。
观察者可能会看到现在所有的砖都是光滑的,而砂浆不是,但请注意,每个砖面似乎是完全平坦的。这看起来不太对,因为砖块的表面通常有些不规则。通过应用凹凸贴图,砖块的阴影法线可以发生变化,这样当它们被渲染时,就不会显得非常光滑。为了计算光照,这种纹理会使矩形的原始表面法线方向发生抖动。
在比较小的夹角来看的话,这种凹凸感会被打破。砖块应该在突出于泥浆,挡住视线,即使从正面看,砖块也应该在砂浆投下阴影。视差贴图使用纹理在渲染平面时使其变形,而视差遮挡贴图将光线投射到高光场纹理上以提高真实感。位移映射通过修改形成模型的三角形高度,真正取代了曲面。图6.1显示了一个带有颜色纹理和凹凸贴图的例子。
这些都是可以用纹理来解决的问题类型的例子,使用越来越复杂的算法。在本章中,纹理技术将被详细介绍。首先,给出了纹理处理的一般框架。接下来,我们将重点放在使用图像来纹理表面,因为这是实时工作中使用的最流行的纹理形式。对程序纹理进行了简要的讨论,并介绍了几种常用的纹理影响曲面的方法。
6.1 纹理管线 The Texturing Pipeline
纹理是一种有效地塑造表面材质和表面光泽度变化的技术。考虑纹理的一种方法是考虑单着色像素的情况。正如在前一章中所看到的,着色是通过考虑材料的颜色和灯光等因素计算出来的。如果存在,透明度也会影响样本。纹理是通过修改着色方程中使用的值来实现的。这些值的变化方式通常基于表面的位置。因此,对于砖墙的例子来说,根据表面位置,将表面上任意一点的颜色替换为砖墙图像中相应的颜色。图像纹理中的像素通常被称为texel,以区别于屏幕上的像素(pixel)。粗糙度纹理修改了粗糙度值,凹凸纹理改变了阴影法线的方向,所以这些都改变了着色方程的结果。
纹理可以用广义纹理管道来描述。稍后将介绍许多术语,但是请记住:将详细描述管道的每个部分。
空间中的位置是纹理处理的起点。这个位置可以在世界空间中,但更多的时候是在模型的参考框架中,所以当模型移动时,纹理也随之移动。使用Kershaw的术语[884],空间中的这个点有一个投影函数,用于获取一组数字,称为纹理坐标,用于访问纹理。这个过程称为映射(mapping),它导致phrase texture mapping。有时纹理图像本身被称为纹理映射,尽管这并不完全正确。
在使用这些新值访问纹理之前,可以使用一个或多个映射函数将纹理坐标转换为纹理空间。这些纹理空间位置用于从纹理中获取值,例如,它们可以是数组索引到图像纹理中以检索像素。然后,通过值转换函数可能再次转换检索到的值,最后,这些新值用于修改表面的某些属性,如材质或着色法线。图6.2详细显示了单个纹理应用程序的这个过程。管道复杂性的原因是,每个步骤都为用户提供了一个有用的部件。应该指出的是,并非所有步骤都需要在任何时候都被激活。
使用这个管道,当一个三角形具有砖墙纹理并在其表面生成一个示例时,就会发生这种情况(参见图6.3)。(x, y, z)在目标局部参照系中的位置;假设是(- 2.3,7.1,88.2)然后将投射函数应用于此位置。就像一幅世界地图是一个三维物体的二维投影,投影函数通常改变(x, y, z)向量成双元素向量(u, v)。本例中使用的投影仪功能相当于一个正射投影(2.3.1节),作用类似幻灯机闪亮的砖墙上三角形的表面形象。要返回到壁面,可以将壁面上的一个点转换成一对值,取值范围从0到1。假设得到的值是(0.32,0.29)。这些纹理坐标将被用来找出在这个位置的图像的颜色。例如,砖块纹理的分辨率是256×256,因此对应函数将(u, v)分别乘以256,得到(81.92,74.24)。去除分数后,砖墙图像中出现像素(81,74),颜色为(0.9,0.8,0.7)。纹理颜色在sRGB颜色空间中,因此如果要在阴影方程中使用颜色,则将其转换为线性空间,给出(0.787,0.604,0.448)(第5.6节)。
6.1.1 投影函数 The Projector Function
纹理处理的第一步是获取曲面的位置,并将其投影到纹理坐标空间中,通常是二维(u, v)空间。建模包通常允许艺术家定义(u, v)每个顶点的坐标。这些可以从投影方法或网格展开算法初始化。美工可以编辑(u, v)坐标,就像编辑顶点位置一样。投影仪的功能通常是通过将空间中的三维点转换为纹理坐标来实现的。建模程序中常用的函数包括球面、圆柱和平面投影[141,88,970]。
其他输入可用于投影函数。例如,表面法线可以用来选择六个平面投影方向中的哪一个用于表面。纹理匹配问题发生在面相接的接缝处;Geiss[521,522]讨论了一种混合它们的技术。Tarini等[1740]描述了多立方体映射,其中模型映射到一组立方体投影,不同体积的空间映射到不同的立方体。
其他投影函数根本不是投影,而是表面创建和棋盘格的一个指导部分。例如,参数曲面有一组(u, v)值作为其定义的一部分。参见图6.4。纹理坐标还可以由各种不同的参数生成,比如视图方向、表面温度或任何可以想象的东西。投影函数的目标是生成纹理坐标。把这些作为位置的函数只是一种方法。
非交互式渲染器通常将这些投影函数作为渲染过程本身的一部分进行调用。一个单一的投影函数可以满足整个模型,但艺术家往往不得不使用工具细分模型,并分别应用不同的投影函数[1345]。参见图6.5。
在实时渲染中,通常在建模阶段使用投影函数,将投影结果存储在顶点上。情况并非总是如此;有时在顶点或像素着色器中应用投影函数是有利的。这样做可以提高精度,并帮助启用各种效果,包括动画(第6.4节)。有些渲染方法,如环境映射(10.4节),具有自己的特殊投影函数,这些函数按像素计算。
球面投影(图6.4中的左边)将点投射到以某个点为中心的假想球体上。这个投影与Blinn和Newell的环境映射方案中使用的是相同的(第10.4.1节),所以407页的方程10.30描述了这个函数。这种投影方法也存在与该节中描述的顶点插值相同的问题。
圆柱投影计算的u纹理坐标与球面投影相同,v纹理坐标计算的是沿圆柱轴线的距离。这种投影对于具有自然轴的物体很有用,比如旋转面。当表面接近垂直于圆柱体轴线时,就会发生畸变。
平面投影就像一束x射线,沿着一个方向平行投影,并将纹理应用于所有表面。它使用正投影(第4.7.1节)。这种类型的投影适用于贴花,例如(第20.2节)。
由于与投影方向平行的表面存在严重的失真,艺术家通常必须手工将模型分解成接近平面的部分。还有一些工具可以通过展开网格或创建一组近乎最优的平面投影来帮助最小化失真,或者帮助这个过程。我们的目标是让每个多边形在纹理的区域中得到更公平的份额,同时尽可能保持网格的连通性。连接性很重要,因为采样工件可以出现在纹理的不同部分相遇的边缘。一个可以很好地展开的网格也可以使艺术家创造作品变得更容易[970,1345]。第16.2.1节讨论了纹理失真对渲染的负面影响。图6.6显示了用于创建图6.5中雕像的工作区。这种展开过程是网格参数化这一更大研究领域的一个方面。感兴趣的读者可以参考Hormann等人的SIGGRAPH课程笔记[774]。
纹理坐标空间并不总是二维平面;有时它是一个三维体。在这种情况下,纹理坐标表示为一个三元素向量(u, v,w), w是沿投影方向的深度。其他系统最多使用四个坐标,通常指定(s, t, r, q) [885];q是齐次坐标中的第四个值。它就像电影或幻灯片放映机一样,投影纹理的大小随着距离的增加而增加。例如,它可以将一种称为gobo的装饰性聚光灯图案投射到舞台或其他表面上[1597]。
纹理坐标空间的另一种重要类型是方向,其中空间中的每个点都由输入方向访问。将这样的空间可视化的一种方法是在一个单位球上的点,每个点上的法线表示用于访问该位置纹理的方向。使用方向参数化的最常见纹理类型是立方体映射(第6.2.4节)。
同样值得注意的是,一维纹理图像和函数也有它们的用途。例如,在地形模型上,颜色可以由高度决定,例如,低地是绿色的;山峰是白色的。线也可以纹理映射;它的一个用途是将雨渲染为一组带有半透明图像纹理的长线条。这样的纹理对于从一个值到另一个值的转换也很有用。例如,作为查找表。
由于一个表面可以应用多个纹理,因此可能需要定义多个纹理坐标集。无论如何应用坐标值,其思想都是一样的:这些纹理坐标在整个表面内插并用于检索纹理值。然而,在插值之前,这些纹理坐标被相应的函数转换。
6.12 映射函数 The Corresponder Function
映射函数将纹理坐标转换为纹理空间位置。它们提供了将纹理应用到表面的灵活性。映射函数的一个例子是使用API选择要显示的现有纹理的一部分;只有这个子图像将在后续操作中使用。
另一种类型的映射器是矩阵变换,它可以应用于顶点着色器或像素着色器。这使移动,旋转,缩放,剪切,或投射纹理的表面。正如第4.1.5节所讨论的,转换的顺序很重要。令人惊讶的是,纹理的转换顺序必须与我们所期望的顺序相反。这是因为纹理变换实际上影响了决定图像位置的空间。图像本身不是被转换的对象;定义图像位置的空间正在更改。
另一类映射函数控制应用图像的方式。我们知道一个图像会出现在(u, v)在[0,1]范围内的表面。但是在这个范围之外会发生什么呢?对应函数决定行为。在OpenGL中,这种类型的通信器函数称为“Wrapping mode”;在DirectX中,它被称为“texture addressing mode”。这类常见的对应函数有:
Wrap(DirectX)、Repeat(OpenGL)或tile——图像在表面上重复自己;算法上,删除纹理坐标的整数部分。这个函数对于让一个物体的图像重复地覆盖一个表面很有用,并且通常是默认值。
mirror——图像在表面上重复,但在每一次重复时都会被镜像(翻转)。例如,图像通常从0到1,然后在1和2之间反转,然后在2和3之间正常,然后反转,以此类推。这提供了沿着纹理边缘的连续性。
clamp(DirectX)或clamp to edge(OpenGL) -范围[0,1]以外的值被钳位到这个范围。这导致图像纹理边缘的重复。当双线性插值发生在纹理边缘附近时,该函数有助于避免意外地从纹理的相对边缘提取样本[885]。
border (DirectX)或clamp to border (OpenGL) -外边的纹理坐标[0,1]用单独定义的边框颜色呈现。例如,这个函数可以很好地在单色表面上呈现贴花,因为纹理的边缘将与边框颜色平滑地混合。
参见图6.7。这些对应的函数可以为每个纹理轴分配不同的值,例如纹理可以沿着u轴重复,也可以夹在v轴上。在DirectX中,还有一个mirror once模式,它沿着纹理坐标的零值对纹理进行一次镜像,然后进行clamp,这对于对称贴花非常有用。
重复平铺纹理是一种为场景添加更多视觉细节的廉价方法。然而,这种方法在重复三次纹理之后,通常看起来并不令人信服,因为眼睛会识别出图案。避免这种周期性问题的常见解决方案是将纹理值与另一个非平铺纹理相结合。这种方法可以得到很大的扩展,如Andersson[40]描述的商业地形绘制系统所示。在该系统中,根据地形类型、高度、坡度等因素,将多种纹理进行组合。纹理图像还与几何模型(如灌木丛和岩石)在场景中的位置相关联。
避免周期性的另一个选择是使用着色器程序来实现特殊的对应函数,这些函数随机地重新组合纹理模式或tile。Wang tiles就是这种方法的一个例子。Wang tile是一组边缘匹配的正方形小瓦片。在纹理处理过程中,瓷砖是随机选择的[1860]。Lefebvre和Neyret[1016]使用依赖的纹理读取和表实现了类似类型的对应函数,以避免模式重复。
最后一个对应函数是隐式的,由图像的大小导出。纹理通常应用在u和v的[0,1]范围内,如砖墙例子所示,将这个范围内的纹理坐标乘以图像的分辨率,就可以得到像素位置。能够在[0,1]范围内指定(u, v)值的好处是可以交换具有不同分辨率的图像纹理,而不必更改存储在模型顶点上的值。
6.1.3 纹理值 Texture Values
在使用相应的函数生成纹理空间坐标后,使用坐标获取纹理值。对于图像纹理,这是通过访问纹理来从图像中检索texel信息来实现的。第6.2节详细讨论了这一过程。图像纹理在实时渲染中占绝大多数,但也可以使用程序化函数。在程序化纹理的情况下,从纹理空间位置获取纹理值的过程不涉及内存查找,而是一个函数的计算。程序纹理将在6.3节中进一步描述。
最直接的纹理值是用于替换或修改表面颜色的RGB三元组;类似地,可以返回单个灰度值。另一种类型的数据返回RGBα,如5.5节所述。α(阿尔法)值通常是不透明的颜色,决定在多大程度上可能影响像素的颜色。也就是说,任何其他值都可以存储,比如表面粗糙度。还有许多其他类型的数据可以存储在图像纹理中,我们将在详细讨论凹凸贴图时看到(第6.7节)。
从纹理返回的值在使用之前可选地进行转换。这些转换可以在着色器程序中执行。一个常见的例子是将数据从无符号范围(0.0到1.0)映射到有符号范围(- 1.0到1.0),用于对存储在颜色纹理中的法线进行着色。
6.2 图像纹理映射 Image Texturing
在图像纹理处理中,二维图像被有效地粘在一个或多个三角形的表面上。我们已经完成了计算文本位置的过程;现在,我们将讨论从给定位置的图像纹理中获取纹理值的问题和算法。在本章的其余部分,图像纹理将被简单地称为纹理。此外,当我们在这里提到像素的单元格时,我们指的是围绕该像素的屏幕网格单元格。正如第5.4.1节所讨论的,像素实际上是一个显示的颜色值,它可以(而且应该,为了更好的质量)受到与其相关的网格单元外部样本的影响。
在本节中,我们特别关注快速采样和过滤纹理图像的方法。第5.4.2节讨论了混叠问题,特别是对象的边缘渲染问题。纹理也可能有采样问题,但它们发生在正在呈现的三角形的内部。
像素着色器通过将纹理坐标值传递给texture2D之类的调用来访问纹理。这些值位于(u, v)纹理坐标中,由对应函数映射到范围[0.0,1.0]。GPU负责将这个值转换为texel坐标。在不同的api中,纹理坐标系有两个主要区别。在DirectX中,纹理的左上角是(0,0),右下角是(1,1)。在OpenGL中,texel(0,0)位于左下角,从DirectX翻转y轴。texel具有整数坐标,但我们通常希望访问texel之间的一个位置并在其中进行混合。这就引出了一个问题:像素中心的浮点坐标是什么。Heckbert[692]讨论了两种可能的系统:截断和舍入。DirectX 9将每个中心定义为(0.0,0.0)—这使用四舍五入。这个系统有点令人困惑,因为在DirectX的原点处,左上角的像素值为(- 0.5,- 0.5)。DirectX 10继续修改OpenGL的系统,其中texel的中心有分数值(0.5,0.5)-截断,或者更准确地说,floor,分数被删除。floor是一个更自然的系统,可以很好地映射到语言,例如,在那个像素(5,9)中,定义了一个范围,u坐标为5.0到6.0,v坐标为9.0到10.0。
当通过texture2D或类似的方法访问纹理时,当像素着色器计算纹理坐标而不是使用顶点着色器传入的未经修改的纹理坐标时,就会发生依赖的纹理读取[66]。注意,这意味着对传入的纹理坐标进行任何更改,即使是交换u和v值这样的简单操作。旧的移动gpu,那些不支持OpenGL ES 3.0的gpu,在着色器没有依赖纹理读取时运行得更有效率,因为随后可以预取texel数据。这个术语的另一个更古老的定义对于早期的桌面gpu尤其重要。在这个上下文中,当一个纹理的坐标依赖于之前一些纹理值的结果时,就会发生依赖纹理读取。例如,一个纹理可能会更改着色法线,这反过来又会更改用于访问多维数据集映射的坐标。这种功能在早期的gpu上是有限的,甚至是不存在的。今天,这种读取可能会影响性能,这取决于在批处理中计算的像素数量以及其他因素。有关更多信息,请参见第23.8节。
gpu中使用的纹理图像大小通常为2m×2n个纹理,其中m和n是非负整数。这些被称为2的幂(POT)纹理。现代gpu可以处理任意大小的非二幂(NPOT)纹理,这允许将生成的图像作为纹理处理。然而,一些较老的移动gpu可能不支持NPOT纹理的mipmapping(第6.2.2节)。图形加速器在纹理大小上有不同的上限。例如,directx12最多允许163842个texel。
假设我们有一个大小为256×256的纹理,并且我们想使用它作为正方形上的纹理。只要投影在屏幕上的正方形与纹理大小大致相同,那么正方形上的纹理看起来就几乎与原始图像一样。但是,如果投影的正方形所覆盖的像素是原始图像所包含像素的十倍(称为放大),或者投影的正方形只覆盖屏幕的一小部分(缩小),又会发生什么呢?答案是,这取决于您决定对这两个单独的情况使用哪种抽样和过滤方法。
本章讨论的图像采样和滤波方法适用于从每个纹理中读取的值。然而,期望的结果是防止最终呈现的图像中的混叠,这在理论上需要采样和过滤最终像素的颜色。这里的区别是过滤着色方程的输入,还是过滤输出。只要输入和输出是线性相关的(这对于输入来说是正确的,比如颜色),那么过滤单独的纹理值就相当于过滤最终的颜色。然而,许多存储在纹理中的着色器输入值,如表面法线和粗糙度值,与输出有非线性关系。标准的纹理过滤方法可能对这些纹理不起作用,导致混叠。第9.13节讨论了过滤这些纹理的改进方法。
6.2.1 放大 Magnification
在图6.8中,一个尺寸为48×48 texels的纹理被纹理化到一个正方形上,相对于纹理大小,这个正方形被看得很近,因此底层的图形系统必须放大纹理。放大最常用的滤波技术是最近邻(实际的滤波器称为框式滤波器box filter——见5.4.1节)和双线性插值(bilinear interpolation)。还有三次卷积,它使用4×4或5×5个纹理数组的加权和。这使得更高的放大质量。虽然本地硬件支持三次卷积(也称为双三次插值 bicubic interpolation)目前并不常见,但它可以在着色器程序中执行。
在图6.8的左侧,使用了最近邻方法。这种放大技术的一个特点是单个纹理可能变得明显。这种效果称为像素化,之所以会发生这种情况,是因为该方法在放大时取距离每个像素中心最近的texel的值,从而产生块状外观。虽然这种方法的质量有时很差,但它只需要为每个像素获取一个texel。
在同一图形的中间图像中,使用双线性插值(有时称为线性插值)。对于每个像素,这种滤波找到四个相邻的纹理,并在二维中进行线性插值,找到像素的混合值。结果更加模糊,使用最近邻方法产生的锯齿已经消失。作为一个实验,试着在眯眼的时候看左边的图片,因为这和低通滤波器的效果差不多,而且更能显示脸部。
回到第170页的砖块纹理示例:在不降低分数的情况下,我们得到(pu, pv) =(81.92, 74.24)。我们在这里使用OpenGL的左下角原点texel坐标系,因为它与标准的笛卡尔坐标系相匹配。我们的目标是在四个最近的texel之间进行插值,使用它们的texel中心定义一个texel大小的坐标系统。参见图6.9。为了找到最近的4个像素,我们从样本位置减去像素中心分数(0.5,0.5),得到(81.42,73.74)。去掉分数后,最近的四个像素的范围从(x, y) =(81,73)到(x+1, y+1) =(82,74)。分数部分(0.42,0.74)对于我们的例子来说,是样本相对于四个texel中心形成的坐标系的位置。我们把这个位置表示为(u‘, v’)
将纹理访问函数定义为t(x, y),其中x和y是整数,返回texel的颜色。任何位置(u ‘, v ‘)的bilinearly插值颜色都可以计算为一个两步过程。首先,texel底部,t (x, y)和t (x + 1, y),将被内插水平(使用u′),最高的两个texel同样,t (x, y + 1)和t (x + 1, + 1)。为texel底部,我们获得(1−u′) t (x, y) + u′t (x + 1, y)(下图绿色圆圈图6.9),顶部,(1−u′) t (x, y + 1) + u′t (x + 1, + 1)(绿色圆圈)。这两个值然后垂直插值(使用v ‘),因此bilinearly插值的颜色b在(pu, pv)处为
直观地说,靠近示例位置的texel会对其最终值产生更大的影响。这就是我们在这个方程中看到的。右上角的texel (x+1, y+1)对u ‘ v ‘有影响。注意对称性:右上角的影响等于左下角与采样点形成的矩形面积。回到我们的示例,这意味着从这个texel检索到的值将乘以0.42×0.74,具体来说就是0.3108。从这个texel顺时针方向,其他乘法器分别为0.42×0.26、0.58×0.26和0.58×0.74,所有四个权值之和为1.0。
对于伴随放大而来的模糊,一个常见的解决方案是使用细节纹理。这些纹理代表了精细的表面细节,从手机上的划痕到地形上的灌木丛。这样的细节被叠加到放大的纹理上,作为一个单独的纹理,在不同的尺度上。细节纹理的高频重复图案,结合低频放大纹理,具有类似于使用单一高分辨率纹理的视觉效果。
双线性插值在两个方向上线性插值。然而,不需要线性插值。例如,一个纹理由棋盘图案中的黑白像素组成。使用双线性插值可以得到不同灰度的纹理样本。通过重新映射,例如,所有低于0.4的灰色都是黑色,所有高于0.6的灰色都是白色,而那些介于两者之间的灰色被拉伸来填补空白,纹理看起来更像棋盘格,同时也给了纹理之间一些混合。参见图6.10。
使用高分辨率纹理也会有类似的效果。例如,假设每个检查器正方形由4×4个文本组成,而不是1×1。围绕每个检查器的中心,插入的颜色将完全是黑色或白色。
在图6.8的右边,使用了一个双三次滤波器,剩余的块度基本上被去掉了。值得注意的是,双三次滤波器比双线性滤波器消耗更高。然而,许多高阶滤波器可以表示为重复线性插值[1518] (也参见第17.1.1节)。因此,纹理单元中用于线性插值的GPU硬件可以通过几个查找来使用。
如果双三次的过滤器被认为过于昂贵,Qu´ılez[1451]提出了一种简单的方法用光滑曲线之间插入一组2×2 texel。我们首先描述曲线,然后是技术。两种常用的曲线是平滑阶跃曲线和五次曲线[1372]:
当您想要平滑地从一个值插入到另一个值时,这些方法非常有用。光滑阶跃曲线具有s ‘ (0) = s ‘(1) = 0的性质,且在0和1之间光滑。五次曲线具有相同的性质,但q ” (0) = q “(1) = 0,即,二阶导数在曲线的起点和终点也是0。这两条曲线如图6.11所示。
该技术首先计算(u ‘, v ‘)(与公式6.1和图6.9中使用的方法相同),首先将样本乘以纹理尺寸并添加0.5。整数部分保留到后面,分数存储在u ‘和v ‘中,其取值范围为[0,1]。然后将(u ‘, v ‘)转换为(tu, tv) = (q(u ‘), q(v ‘)),仍然在[0,1]的范围内。最后,减去0.5,再把整数部分加回去;然后得到的u坐标除以纹理宽度,与v类似,此时,新的纹理坐标与GPU提供的双线性插值查找一起使用。注意,这个方法将在每个texel处给出一个平台,这意味着,如果texel位于RGB空间中的一个平面上,那么这种类型的插值将会给出一个光滑的,但仍然是阶梯状的外观,这可能并不总是需要的。参见图6.12。
6.2.2 缩小 Minification
当纹理最小化时,几个纹理可能覆盖一个像素的单元格,如图6.13所示。要为每个像素获得正确的颜色值,您应该集成纹理对像素的影响。然而,要精确地确定一个特定像素附近的所有文本的确切影响是困难的,而且实际上不可能实时完美地做到这一点。
由于这个限制,在gpu上使用了几种不同的方法。一种方法是使用最近邻,其工作原理与相应的放大滤波器完全相同,即,它选择在像素单元的中心可见的texel。这种滤波器可能会导致严重的混叠问题。在图6.14中,最上面的图中使用了最邻近纹理。在水平线处,由于影响一个像素的众多纹理中只有一个被选择来表示表面,所以出现了伪影。当表面相对于观察者移动时,这样的伪影更加明显,并且是所谓的累积混叠(temporal aliasing)的一种表现。
另一种常用的滤波器是双线性插值,其工作原理与放大滤波器完全相同。这个过滤器只比最小化的最近邻方法稍微好一点。它混合了四个纹理而不是仅仅使用一个,但是当一个像素受到四个以上的纹理的影响时,过滤器很快就会失效并产生混叠。
更好的解决方案是可能的。如5.4.1节所述,混叠问题可以通过采样和滤波技术来解决。纹理的信号频率取决于其纹理在屏幕上的间距。由于Nyquist限制,我们需要确保纹理的信号频率不大于采样频率的一半。例如,假设一个图像是由交替的黑白线组成的,中间有一个texel。然后波长是两个texel宽(从黑线到黑线),所以频率是1/2。若要在屏幕上正确显示此纹理,则频率必须至少为2×1/2,即,每个texel至少有一个像素。所以,对于一般的纹理,每个像素最多应该有一个texel来避免混叠。
为了达到这个目的,要么提高像素的采样频率,要么降低纹理频率。 前一章讨论的抗混叠方法给出了提高像素采样率的方法。然而,这些只给出了有限的增加采样频率。为了更好地解决这一问题,各种纹理细化算法应运而生。
所有纹理反锯齿算法的基本思想都是一样的:预处理纹理并创建数据结构,这些数据结构将帮助计算一组纹理对像素的效果的快速近似。对于实时渲染,这些算法具有使用固定时间和资源执行的特点。通过这种方法,每个像素都有固定数量的样本,并结合起来计算(潜在的巨大)数量的texel的效果。
Mipmapping
最流行的纹理抗混叠方法是mipmapping[1889]。它以某种形式在现在生成的所有图形加速器上实现。“Mip”在parvo中代表multum,在拉丁语中是“很多东西在一个小地方”的意思——这是一个很好的名字,用来描述一个过程,在这个过程中,原始的纹理被反复过滤成更小的图像。
当使用mipmapping最小化过滤器时,在实际呈现之前,原始纹理会被一组更小版本的纹理增强。纹理(在0级)被向下采样到原始区域的四分之一,每个新的texel值通常计算为原始纹理中四个相邻texel的平均值。新的一级纹理有时被称为原始纹理的子纹理。递归地执行缩减,直到纹理的一个或两个维度都等于一个texel。这个过程如图6.15所示。作为一个整体的图像集通常被称为mipmap链。
形成高质量mipmap的两个重要因素是良好的滤波和伽马校正。形成mipmap级别的常用方法是取每个2×2组texel,并对它们进行平均,得到mip texel值。所使用的过滤器是一个盒子过滤器,可能是最差的过滤器之一。这可能导致质量很差,因为它会不必要地模糊低频,同时保留一些导致混叠的高频[172]。最好使用高斯、Lanczos、Kaiser或类似的过滤器;有很多解决这个问题的快速,免费的源代码[172,1592],和一些api支持更好的滤波器内置在GPU。在纹理的边缘附近,过滤时必须注意纹理是重复的还是单一的复制。
对于非线性空间中编码的纹理(如大多数颜色纹理),在过滤时忽略gamma校正将修改mipmap级别的感知亮度[173,607]。当你离物体越远,使用未校正的mipmap,物体整体看起来就越暗,对比度和细节也会受到影响。因此,将这些纹理从sRGB转换为线性空间(第5.6节),在该空间中执行所有mipmap过滤,并将最终结果转换回sRGB颜色空间进行存储是非常重要的。大多数api都支持sRGB纹理,因此可以在线性空间中正确生成mipmap并将结果存储在sRGB中。当访问sRGB纹理时,首先将它们的值转换为线性空间,以便正确地执行放大和缩小。
正如前面提到的,一些纹理与最终的阴影颜色有着基本的非线性关系。虽然这在一般情况下会造成过滤问题,但是mipmap生成对这个问题特别敏感,因为要过滤成百上千个像素。为了获得最佳结果,通常需要使用专门的mipmap生成方法。这些方法详见第9.13节。
访问这个结构的基本过程,而纹理是直接的。屏幕像素在纹理本身上包围一个区域。当像素的区域投射到纹理上时(图6.16),它包含一个或多个纹理。使用像素的单元格边界并不完全正确,但是这里使用它是为了简化表示。单元格外的texel可以影响像素的颜色;5.4.1之前看到的部分。目标是大致确定纹理对像素的影响程度。有两种常见的措施用来计算d(OpenGL称之为λ,也被称为纹理的细节)。一种是利用像素单元形成的较长的四边形边缘来近似像素的覆盖范围[1889];另一个是将∂u/∂x,∂v/∂x,∂u/∂y,∂v/∂y这四个∂的绝对值的最大值作为测量值[901,1411]。每个差分是纹理坐标相对于屏幕轴的变化量的度量。例如,∂u/∂x是一个像素的u纹理值沿屏幕x轴的变化量。有关这些方程的更多信息,请参见Williams的原始文章[1889]或Flavell的文章[473]或Pharr的文章[1411]。McCormack等人[1160]讨论了用最大绝对值法引入混叠,他们给出了一个替代公式。Ewins等[454]分析了几种质量相当的算法的硬件成本。
这些梯度值可用于使用shader Model 3.0或更新版本的像素着色器程序。由于它们是基于相邻像素值之间的差异,因此在受动态流控制影响的像素着色器部分(第3.8节)是不可访问的。对于要在这样的部分中执行的纹理读取(例如,在循环中),必须更早地计算导数。注意,由于顶点着色器不能访问梯度信息,梯度或细节级别需要在顶点着色器本身计算,并在使用顶点纹理时提供给GPU。
计算坐标d的目的是确定沿着mipmap的金字塔轴在哪里采样。参见图6.15。目标是像素与texel的比例至少为1:1,以实现奈奎斯特速率。这里的重要原则是,当像素单元包含更多的纹理和d时,将访问一个更小、更模糊的纹理版本。(u, v, d)三元组用于访问mipmap。值d类似于纹理级别,但它不是一个整数值,而是级别之间距离的分数值。对d位置上的纹理层和下的纹理层进行采样。(u, v)位置用于从这两个纹理级别中的每一个检索bilinearly插值样本。然后根据每个纹理层到d的距离,对得到的样本进行线性插值。整个过程称为三线性插值,每像素执行一次。
d坐标上的一个用户控件是细节偏差级别(LOD偏差)。这是一个添加到d的值,因此它影响纹理的相对感知锐度。如果我们进一步向上移动金字塔开始(增加d),纹理将看起来更加模糊。对于任何给定的纹理,良好的LOD偏差都会随着图像类型和使用方式的不同而变化。例如,一开始有些模糊的图像可能使用负偏置,而用于纹理处理的合成图像过滤不良(别名)可能使用正偏置。可以在像素着色器中为纹理整体或每个像素指定偏置。对于更精细的控制,用户可以提供用于计算它的d坐标或导数。
mipmapping的好处是,不需要分别对影响像素的所有texel进行求和,而是访问和插值预组合的texel集。这个过程需要一定的时间,无论缩小多少。然而,mipmapping有几个缺陷[473]。一个主要问题是过度模糊。假设一个像素单元在u方向上覆盖了大量的texel,而在v方向上只覆盖了texel。这种情况通常发生在观察者沿纹理表面几乎是边缘朝上看时。事实上,可能需要沿着纹理的一个轴进行缩小,然后沿着另一个轴进行放大。访问mipmap的效果是检索纹理上的正方形区域;检索矩形区域是不可能的。为了避免混叠,我们选择纹理上像素单元的近似覆盖率的最大度量。这导致检索到的样本通常比较模糊。这种效果可以在图6.14中的mipmap图像中看到。向右移动的线显示出过度模糊。
总面积表 Summed-Area Table
避免过度模糊的另一种方法是求和区域表(sum-area table, SAT)[312]。要使用此方法,首先创建一个与纹理大小相同的数组,但包含存储的颜色的更多精度(例如,红色、绿色和蓝色各16位或更多)。在这个数组的每个位置,必须计算和存储由这个位置和texel(0,0)(原点)组成的矩形中所有对应纹理的texel的和。在纹理处理过程中,像素单元在纹理上的投影被一个矩形绑定。然后访问sum-area表来确定这个矩形的平均颜色,该颜色作为像素的纹理颜色传递回来。使用图6.17所示矩形的纹理坐标计算平均值。使用式6.3所示公式:
这里,x和y是矩形的texel坐标,s[x, y]是该texel的和面积值。这个方程的工作原理是从右上角到原点的整个面积的和,然后减去A和B的面积减去相邻角的贡献。面积C减去了两次,所以它是由左下角加回来的。注意(xll, yll)是C区域的右上角,即, (xll + 1, yll + 1)为边界框的左下角。
使用求和区域表的结果如图6.14所示。接近地平线的线在右边边缘更清晰,但是中间的对角线交叉线仍然过于模糊。问题是,当一个纹理沿着它的对角线进行观察时,会生成一个大的矩形,其中许多纹理都不位于正在计算的像素附近。例如,想象一个长而薄的矩形,它代表像素单元的反投影,在图6.17中对角线地横过整个纹理。将返回整个纹理矩形的平均值,而不仅仅是像素单元内的平均值。
面积和表是所谓的各向异性滤波算法的一个例子[691]。这种算法在非正方形的区域上检索texel值。然而,SAT能够在主要水平和垂直方向上最有效地做到这一点。还要注意,对于大小为16×16或更小的纹理,和区域表至少占用两倍的内存,对于较大的纹理,需要更高的精度。摘要面积表可以在现代gpu上实现,它以合理的总体内存成本提供更高的质量[585]。改进的滤波对高级渲染技术的质量至关重要。例如,Hensley等人[718,719]提供了一种有效的实现方法,并展示了如何对面积进行累加采样来改善光泽反射。其他使用面积采样的算法可以通过SAT得到改进,如景深[585,719],阴影映射[988],模糊反射[718]。
无约束各向异性过滤 Unconstrained Anisotropic Filtering
对于目前的图形硬件,进一步改进纹理滤波最常用的方法是重用现有的mipmap硬件。其基本思想是将像素单元反投影,然后对纹理上的这个四边形(quad)进行多次采样,并将这些采样组合在一起。如上所述,每个mipmap示例都有一个位置和一个与之相关的方形区域。该算法不是使用单一的mipmap样本来近似这个四边形的覆盖范围,而是使用几个正方形来覆盖这个四边形。短边的四分之一可以用来确定d(不像在mipmapping中,通常使用较长的边);这使得每个mipmap样本的平均面积更小(因此更不模糊)。四边形的长边用于创建一条各向异性线,平行于长边并穿过四边形的中间。当各向异性的数量在1:1和2:1之间时,沿着这条线取两个样本(见图6.18)。在各向异性比例较高的情况下,沿轴取的样品较多。该方案允许各向异性线在任意方向运行,不受面积和表的限制。它也不需要比mipmap更多的纹理内存,因为它使用mipmap算法进行采样。各向异性滤波的一个例子如图6.19所示。
这种沿轴采样的想法最早是由Schilling等人通过他们的Texram动态存储设备提出的[1564]。Barkans描述了算法在Talisman系统中的使用[103]。McCormack等人[1161]提出了一种类似的系统,称为Feline。Texram的原始公式中,各向异性轴anisotropic anix(也称为探针probes)上的样品具有相同的权重。Talisman赋予轴两端的两个探针一半的权重。Feline使用高斯滤波器内核核来对一组探针进行加权。这些算法接近软件采样算法的高质量,如椭圆加权平均(EWA)滤波器,它将像素的影响区域转化为对纹理的椭圆,并通过滤波核对椭圆内的纹理进行加权[691]。Mavridis和Papaioannou提出了几种在GPU上使用着色器代码实现EWA过滤的方法[1143]。
6.2.3 体积贴图 Volume Textures
图像纹理的一个直接扩展是由(u, v,w)(或(s, t, r)值访问的三维图像数据。例如,医学成像数据可以生成一个三维网格;通过在网格中移动一个多边形,可以查看这些数据的二维切片。一个相关的想法是用这种形式表示体积光。在一个表面上的一个点上的照明是通过找到它在这个体积内的位置的值,结合光的方向来发现的。
大多数gpu支持卷纹理的mipmapping。由于在体纹理的单个mipmap级别内的过滤涉及到三线性插值,因此在mipmap级别之间的过滤需要四线性插值。由于这涉及到对16个纹理的结果进行平均,可能会产生精度问题,可以通过使用更高精度的体纹理来解决。Sigg和Hadwiger[1638]讨论了这个问题和其他与体纹理相关的问题,并提供了执行过滤和其他操作的有效方法。
尽管卷纹理有更高的存储要求,而且过滤成本更高,但它们确实有一些独特的优势。由于三维位置可以直接用作纹理坐标,因此可以跳过为三维网格寻找良好的二维参数化的复杂过程。这避免了通常使用二维参数化出现的失真和接缝问题。体纹理还可以用来表示木材或大理石等材料的体结构。模型纹理与这样的纹理将出现从这种材料雕刻。
使用体纹理进行表面纹理是非常低效的,因为绝大多数采样都没有使用。Benson和Davis[133]以及DeBry等[334]讨论了在稀疏八叉树结构中存储纹理数据的问题。该方案非常适合交互式三维绘画系统,因为曲面在创建时不需要指定明确的纹理坐标,并且八叉树可以将纹理细节控制在任何需要的水平。Lefebvre等[1017]讨论了在现代GPU上实现八叉树纹理的细节。Lefebvre和Hoppe[1018]讨论了一种将稀疏体数据打包成明显更小的纹理的方法。
6.2.4 Cube Maps
另一种类型的纹理是Cube Texture或Cube Map,它有六个方形纹理,每个都与一个立方体的一个面相关联。使用一个三分量纹理坐标矢量访问一个立方体映射,该矢量指定从立方体中心向外的射线的方向。射线与立方体相交的点如下所示。大小最大的纹理坐标选择对应的面(例如,向量(-3.2,5.1,-8.4)选择-z面)。其余两个坐标除以最大星等坐标的绝对值,即8.4点。现在它们的范围从- 1到1,并且简单地重映射到[0,1]以便计算纹理坐标。例如,坐标(−3.2,5.1)映射到((−3.2/8.4 + 1)/ 2,(5.1/8.4 + 1)/ 2) ≈ (0.31,0.80)。立方体映射对于表示方向函数的值很有用;它们最常用于环境映射(第10.4.3节)。
6.2.5 纹理表示 Texture Representation
在应用程序中处理许多纹理时,有几种方法可以提高性能。纹理压缩在第6.2.6节中进行了描述,而本节的重点是纹理图集(texture atlases)、纹理数组(texture arrays)和无绑定纹理(bindless textures),所有这些都是为了避免在呈现时更改纹理的成本。在19.10.1和19.10.2节中,描述了纹理流和代码转换。
为了能够为GPU批量处理尽可能多的工作,通常倾向于尽可能少地更改状态(第18.4.2节)。为此,可以将多个图像放入一个更大的纹理中,称为纹理图集。这在图6.20的左边进行了说明。注意,子纹理的形状可以是任意的,如图6.6所示。子纹理布局地集的优化由N¨oll和Stricker[1286]描述。还需要注意mipmap的生成和访问,因为mipmap的上层可能包含几个独立的、不相关的形状。Manson和Schaefer[1119]提出了一种考虑曲面参数化的优化mipmap创建方法,该方法可以产生更好的结果。Burley和Lacewell[213]提出了一种叫做Ptex的系统,在这个系统中,细分表面上的每个四边形都有自己的小纹理。这样做的好处是避免了在网格上分配唯一的纹理坐标,并且在纹理图集的断开部分的接缝上没有瑕疵。为了能够跨四元组过滤,Ptex使用邻接数据结构。当最初的目标是渲染时,Hillesland[746]介绍了压缩的Ptex,它将每个面的子纹理放入一个纹理图集中,并使用相邻面的填充来避免过滤时的间隔性。Yuksel[1955]提出了网格颜色纹理,它改进了Ptex。Toth[1780]为类ptex系统提供了高质量的跨面过滤,它实现了一种方法,如果过滤锥不在[0,1]2的范围内,则丢弃过滤锥。
使用图集的一个困难是Wrap/Repeat和Mirror模式,这不会正确地影响子纹理,但只会影响整个纹理。另一个问题可能发生在为一个图集生成mipmaps时,其中一个子纹理可能渗入另一个子纹理。然而,在将每个子纹理放入一个大型纹理图集之前,分别为每个子纹理生成mipmap层次结构,并对子纹理使用2的幂次分辨率,可以避免这种情况[1293]。
解决这些问题的一个更简单的方法是使用称为纹理数组的API构造,它完全避免了mipmapping和重复模式的任何问题[452]。参见图6.20的右边部分。纹理数组中的所有子纹理都需要具有相同的维度、格式、mipmap层次结构和MSAA设置。和纹理图集一样,设置一个纹理数组只需要执行一次,然后可以使用着色器中的索引访问任何数组元素。这比绑定每个子纹理快5倍[452]。
一个可以帮助避免状态更改成本的特性是对无绑定纹理的API支持[1407]。如果没有无绑定纹理,则使用API将纹理绑定到特定的纹理单元。一个问题是纹理单元数量的上限,这使程序员的事情变得复杂。驱动程序确保纹理驻留在GPU端。对于无绑定的纹理,没有纹理数量的上限,因为每个纹理仅由64位指针(有时称为句柄)与其数据结构关联。这些句柄可以通过许多不同的方式访问,例如,通过uniforms,通过varying data,从其他纹理,或从着色器存储缓冲对象(SSBO)。应用程序需要确保纹理驻留在GPU端。无绑定纹理避免了驱动程序中的任何类型的绑定成本,这使得渲染速度更快。
6.2.6 贴图压缩 Texture Compression
一种直接解决内存和带宽问题以及缓存问题的解决方案是固定速率纹理压缩[127]。通过GPU实时解码压缩纹理,纹理可以占用更少的纹理内存,从而增加有效的缓存大小。至少同样重要的是,这样的纹理使用起来更高效,因为它们在访问时消耗的内存带宽更少。一个相关但不同的用例是为了提供更大的纹理而添加压缩。例如,在5122分辨率下,每个texel使用3个字节的非压缩纹理将占用768 kB。使用纹理压缩,压缩比为6:1,10242纹理只占用512 kB。
图像文件格式(如JPEG和PNG)中使用了多种图像压缩方法,但是在硬件中实现对这些方法的解码非常昂贵(关于纹理转码的信息,请参阅19.10.1节)。S3开发了一种称为S3纹理压缩(S3TC)的方案[1524],该方案被选为DirectX的标准,称为dxtcin DirectX 10,称为BC(块压缩)。此外,它是OpenGL中事实上的标准,因为几乎所有gpu都支持它。它的优点是创建一个大小固定、独立编码的压缩图像,并且解码简单(因此也快速)。图像的每个压缩部分都可以独立于其他部分处理。没有共享查找表或其他依赖项,这简化了解码。
DXTC/BC压缩方案有七种变体,它们具有一些共同的特性。编码是在4×4的texel块上完成的,也称为tile。每个块分别编码。编码基于插值。对于每个编码的量,存储两个参考值(例如,颜色)。为块中的16个texel中的每个保存一个插值因子。它在两个参考值之间的直线上选择一个值,例如,一个颜色等于或从两个存储的颜色中插入。压缩来自于仅存储两种颜色以及每个像素的短索引值。
在表6.1中总结了这7种变体之间的精确编码。注意,“DXT”表示DirectX 9中的名称,“BC”表示DirectX 10及以上的名称。从表中可以看出,BC1有两个16位参考RGB值(5位红色,6位绿色,5位蓝色),每个texel都有一个2位插值因子,可以从一个参考值或两个中间值中选择。与未压缩的24位RGB纹理相比,这表示纹理压缩比为6:1。BC2以与BC1相同的方式编码颜色,但是每个texel (bpt)为量子化(raw) alpha添加4位。对于BC3,每个块都以与DXT1块相同的方式编码RGB数据。此外,alpha数据使用两个8位参考值和一个3位插值因子进行编码。每个texel可以选择一个参考alpha值或六个中间值中的一个。BC4有一个通道,在BC3中编码为alpha。BC5包含两个通道,其中每个通道编码为BC3。
BC6H用于高动态范围(HDR)纹理,其中每个texel最初在每个R、G和B通道上都有16位浮点值。这种模式使用16个字节,结果是8个bpt。它为单行提供一种模式(类似于上面的技术),为两行提供另一种模式,其中每个块可以从一小组分区中选择。两种参考颜色也可以进行delta编码以获得更高的精度,并且根据使用的模式也可以有不同的精度。在BC7中,每个块可以有1到3行代码,并存储8个bpt。目标是高质量的纹理压缩8位RGB和RGBA纹理。它与BC6H共享许多属性,但是是LDR纹理的格式,而BC6H是HDR的格式。注意,在OpenGL中,BC6H和BC7分别称为BPTC FLOAT和BPTC。这些压缩技术可以应用于立方体或体纹理,以及二维纹理。
这些压缩方案的主要缺点是它们是有损的。也就是说,通常无法从压缩版本中检索原始图像。在BC1-BC5中,只有4或8个插值值用于表示16个像素。如果一个tile中有大量不同的值,就会有一些损失。在实践中,如果使用正确,这些压缩方案通常可以提供可接受的图像保真度。
BC1-BC5的问题之一是,用于块的所有颜色都位于RGB空间中的直线上。例如,红色、绿色和蓝色不能在一个块中表示。BC6H和BC7支持更多的线路,因此可以提供更高的质量。
对于OpenGL ES,选择了另一种压缩算法Ericsson texture compression (ETC)[1714]来包含在API中。该方案与S3TC一样具有解码速度快、随机存取、无间接查找、速率固定等特点。它将一个4×4的文本块编码为64位,即,每个texel使用4位。基本思想如图6.21所示。每个2×4块(或4×2块,取决于哪个块质量最好)存储一个基本颜色。每个块还从一个小型静态查找表中选择一组四个常量,块中的每个texel都可以选择添加该表中的一个值。这将修改每个像素的亮度。图像质量与DXTC相当。
在ETC2[1715]中,OpenGL ES 3.0中包含了未使用的位组合,为原有的ETC算法添加了更多的模式。未使用的位组合是压缩表示(例如,64位),它解压缩为与另一个压缩表示相同的图像。例如,在BC1中,将两个引用颜色设置为相同是没有用的,因为这将表示一个常量颜色块,而只要一个引用颜色包含该常量颜色,就可以得到该常量颜色块。在ETC中,一种颜色也可以从带有符号号的第一种颜色编码为delta,因此计算可以溢出或下溢。这种情况被用来信号其他压缩模式。ETC2增加了两种新的模式,每种模式有四种颜色,每种颜色的派生方式不同,最后一种模式是RGB空间中的一个平面,用于处理平滑的转换。Ericsson alpha compression (EAC)[1868]压缩只有一个组件的图像(e.g, alpha).这种压缩类似于基本ETC压缩,但只针对一个组件,生成的图像每个texel存储4位。它可以选择与ETC2结合使用,此外,还可以使用两个EAC通道来压缩法线(更多内容见下面的主题)。所有ETC1、ETC2和EAC都是OpenGL 4.0核心profile、OpenGL ES 3.0、Vulkan和Metal的一部分。
压缩法线贴图(在第6.7.2节中讨论)需要注意。为RGB颜色设计的压缩格式通常不适用于普通的xyz数据。大多数方法都利用了法线已知为单位长度这一事实,并进一步假设其z分量为正(对于切线空间法线来说,这是一个合理的假设)。这只允许存储法线的x和y分量。z分量是动态地派生出来的:
这本身会导致适度的压缩,因为只存储两个组件,而不是三个。由于大多数gpu本身并不支持三组件纹理,这也避免了浪费组件的可能性(或者必须在第四个组件中打包另一个数量)。进一步的压缩通常通过以BC5 / 3Dc-format的纹理存储x和y组件来实现。参见图6.22。由于每个块的参考值限定了最小和最大的x和y分量值,因此可以将它们视为在xy平面上定义了一个边界框。3位插值因子允许在每个轴上选择8个值,因此边界框被划分为一个8×8的可能法线网格。或者,可以使用EAC的两个通道(用于x和y),然后计算上面定义的z。
在不支持BC5/3Dc或EAC格式的硬件上,一个常见的备用方法[1227]是使用dxt5格式纹理,并将两个组件存储为绿色和alpha组件(因为这些组件的存储精度最高)。另外两个组件未使用。
PVRTC[465]是一种纹理压缩格式,可以在Imagination Technologies的硬件PowerVR上使用,它最广泛的应用是在iphone和ipad上。它为每个texel提供了2位和4位的方案,并压缩了4×4个texel的块。其关键思想是提供图像的两个低频(平滑)信号,这两个信号是通过相邻的texel数据块和插值得到的。然后,使用每个texel 1或2位元对图像上的两个信号进行插值。
自适应可伸缩纹理压缩(Adaptive scalable texture compression, ASTC)[1302]的不同之处在于,它将一个n×m的纹理块压缩成128位。块大小从4×4到12×12不等,这导致了不同的比特率,最低为0.89位/ texel,最高为8位/ texel。ASTC为紧凑索引表示使用了多种技巧,每个块可以选择行数和端点编码。此外,ASTC可以处理每个纹理1-4个通道,以及LDR和HDR纹理。ASTC是OpenGL ES 3.2及更高版本的一部分。
上面给出的所有纹理压缩方案都是有损的,当压缩一个纹理时,可以在这个过程中花费不同的时间。在压缩上花费几秒钟甚至几分钟,就可以获得更高的质量;因此,这通常是作为离线预处理完成的,并存储起来供以后使用。或者,你可以只花几毫秒的时间,因此质量较低,但纹理可以被压缩在接近实时和立即使用。一个例子是skybox(第13.3节),它大约每隔一秒就会重新生成一次,此时云可能已经轻微移动了。解压非常快,因为它是使用固定功能硬件完成的。这种差异称为数据压缩不对称,在这种情况下,压缩可以而且确实需要比解压缩长得多的时间。
Kaplanyan[856]提出了几种提高压缩纹理质量的方法。对于包含颜色和法线贴图的纹理,建议使用每个组件16位元来创建贴图。对于颜色纹理,然后执行直方图重新标准化(对这16位),然后在着色器中使用比例和偏置常数(每个纹理)反转直方图的效果。直方图归一化是将图像中使用的值分散到整个范围内的一种技术,是一种有效的对比度增强方法。每个组件使用16位元,可以确保直方图在重新标准化之后没有未使用的槽,这减少了许多纹理压缩方案可能引入的带状伪迹。如图6.23所示。此外,Kaplanyan建议,如果75%的像素在116/255以上,那么纹理使用线性颜色空间,否则将纹理存储在sRGB中。对于法线贴图,他还指出BC5/3Dc经常独立于y压缩x,这意味着并不总是能找到最好的法线。相反,他建议对法线使用以下误差度量:
其中n为原始法线,nc为相同的法线压缩后再解压缩。
需要注意的是,也可以在不同的颜色空间压缩纹理,这可以用来加快纹理压缩。常用的变换是RGB→YCoCg [1112]:
其中Y是亮度项Co和Cg是色度项。逆变换也很便宜:
这相当于增加了一些内容。这两个变换是线性的,从方程6.6可以看出,它是一个矩阵-向量乘法,它本身是线性的(见方程4.1和4.2)。这很重要,因为可以存储YCoCg,而不是在纹理中存储RGB;纹理硬件仍然可以在YCoCg空间中执行过滤,然后像素着色器可以根据需要转换回RGB。应该注意的是,这种转换本身是有损的,这可能无关紧要。
还有一种可逆的RGB→YCoCg变换,总结为:
这意味着可以在24位RGB颜色和相应的YCoCg表示之间来回转换,而不会有任何损失。需要注意的是,如果RGB中的每个分量都有n位元,那么Co和Cg都有n + 1位元,以保证可逆变换;不过Y只需要n位。Van Waveren Casta˜no[1852]使用有损YCoCg变换来实现快速压缩DXT5 / BC3 CPU或GPU。它们将Y存储在alpha通道中(因为它的精度最高),而Co和Cg存储在RGB的前两个组件中。压缩变得很快,因为Y是单独存储和压缩的。对于Co-和cg组件,他们找到一个二维边界框并选择产生最佳结果的框对角线。注意,对于在CPU上动态创建的纹理,最好也在CPU上进行纹理压缩。当纹理通过GPU渲染创建时,通常最好也在GPU上进行纹理压缩。YCoCg变换和其他亮度-色度变换常用于图像压缩,其中色度分量的平均值超过2×2像素。这样可以减少50%的存储空间,而且通常效果很好,因为色度变化比较慢。Lee-Steere和Harmon[1015]进一步将其转换为色调饱和度值(hue- satur- value, HSV),在x和y中将色调和饱和度的采样值降低4倍,并将值存储为单个通道DXT1纹理。Van Waveren andCasta˜no也描述了快速压缩法线贴图[1853]的方法。
Griffin和Olano[601]的研究表明,当多个纹理应用于具有复杂阴影模型的几何模型时,纹理的质量往往很低,没有任何可感知的差异。因此,根据用例,降低质量是可以接受的。Fauconneau[463]提出了一种针对directx11纹理压缩格式的SIMD实现。
6.3 程序化贴图 Procedural Texturing
给定纹理空间位置,执行图像查找是生成纹理值的一种方法。另一种方法是对函数求值,从而定义过程纹理。
虽然过程纹理通常用于离线渲染应用程序,但图像纹理在实时渲染中更为常见。这是因为现代gpu中图像纹理硬件的极高效率,可以在一秒钟内执行数十亿次纹理访问。然而,GPU架构正在向更便宜的计算和(相对)更昂贵的内存访问发展。这些趋势使得过程纹理在实时应用程序中得到了更大的应用。
考虑到体图像纹理的高存储成本,体纹理是一个特别有吸引力的程序纹理应用程序。这种纹理可以通过多种技术合成。最常见的一种方法是使用一个或多个噪声函数来生成值[407,1370,1371,1372]。参见图6.24。噪声函数通常以连续的2次幂频率采样,称为八度(octaves)。每个八度都有一个权值,通常随着频率的增加而下降,这些加权样本的和称为湍流函数(turbulence function)。
由于计算噪声函数的代价较大,三维阵列中的格点通常是预先计算好的,用来插值纹理值。使用颜色缓冲混合快速生成这些数组的方法有很多[1192]。Perlin[1373]提出了一种快速、实用的采样该噪声函数的方法,并展示了一些用途。Olano[1319]提供了噪声生成算法,允许在存储纹理和执行计算之间进行权衡。McEwan等人[1168]开发了不需要任何查找就可以计算着色器中的经典噪声和单纯噪声的方法,并提供了源代码。Parberry[1353]使用动态规划将计算分摊到几个像素上,以加速噪声计算。Green[587]提供了一种更高质量的方法,但它更适合于近交互应用程序,因为它使用50像素着色器指令进行一次查找。可以对Perlin[1370, 1371, 1372]提出的原始噪声函数进行改进。Cook和DeRose[290]提出了另一种表示方法,称为小波噪声(wavelet noise),它避免了混叠问题,只增加了很小的评估成本。Liu等[1054]使用多种噪声函数来模拟不同的木材纹理和表面饰面。我们还推荐Lagae等人[956]关于这一主题的最新报告。
其他的程序方法是可能的。例如,通过测量从每个位置到散布在空间中的一组“特征点”的距离,就形成了一个细胞纹理。以不同的方式映射结果的最近距离,例如,改变颜色或正常阴影,创建看起来像细胞、石板、蜥蜴皮肤和其他自然纹理的模式。Griffiths[602]讨论了如何高效地找到最近的邻居并在GPU上生成细胞纹理。
另一种类型的程序纹理是物理模拟的结果,或者是一些其他交互过程的结果,例如水波或扩展裂缝。在这种情况下,过程纹理可以有效地在动态条件下产生无限的变化。
在生成过程性的二维纹理时,参数化问题可能比创建纹理带来更大的困难,在创建纹理时,可以手动修改或修改拉伸或接缝瑕疵。一种解决方案是通过直接在表面合成纹理来完全避免参数化。在复杂表面上进行这种操作在技术上具有挑战性,是一个活跃的研究领域。有关这一领域的概述,请参见Wei等人[1861]。
反走样过程纹理比反走样图像纹理既困难又容易。一方面,像mipmapping这样的预计算方法是不可用的,这给程序员带来了负担。另一方面,过程纹理作者拥有关于纹理内容的“内部信息”,因此可以对其进行调整以避免混叠。这对于通过合并多个噪声函数创建的过程纹理尤其正确。每个噪声函数的频率都是已知的,因此任何可能导致混叠的频率都可以被丢弃,这实际上降低了计算的成本。有各种各样的技术来消除其他类型的过程纹理的混叠[407,605,1392,1512]。Dorn等[371]讨论了以前的工作,并提出了一些重新构造纹理函数以避免高频的过程,即,以限制带宽。
6.4 纹理动画 Texture Animation
应用于表面的图像不一定是静态的。例如,视频源可以用作帧与帧之间变化的纹理。
纹理坐标也不需要是静态的。应用程序设计器可以显式地在帧与帧之间更改纹理坐标,或者在网格的数据本身中更改,或者通过在顶点着色器或像素着色器中应用的函数更改。想象一下,一个瀑布已经被建模,并且它已经被一个看起来像落水的图像纹理化了。假设v坐标是流体的方向。要使水运动,必须从每一帧的v坐标中减去一个量。从纹理坐标中减去会使纹理本身看起来向前移动。
通过将矩阵应用到纹理坐标,可以创建更精细的效果。除了平移,这还允许线性变换,如缩放、旋转和剪切[1192,1904]、图像翘曲和变形变换[1729]和广义投影[638]。通过在CPU或着色器中应用函数,可以创建许多更精细的效果。
通过使用纹理混合技术,可以实现其他动画效果。例如,从大理石纹理开始,逐渐淡化为肉质纹理,就可以使雕像栩栩如生[1215]。
6.5 材质映射 Material Mapping
纹理的一个常见用途是修改影响着色方程的材质属性。现实世界中的物体通常具有不同表面的物质属性。为了模拟这样的对象,像素着色器可以从纹理中读取值,并在计算阴影方程之前使用它们修改材质参数。最常被纹理修改的参数是表面颜色。这种纹理被称为albedo color map或漫diffuse color map。但是,任何参数都可以通过纹理进行修改:替换它、乘以它或以其他方式更改它。例如,在图6.25中,三个不同的纹理被应用到一个表面,替换了常量值。
纹理在材料中的使用可以更进一步。纹理可以用来控制像素着色器本身的流和功能,而不是修改方程中的参数。两个或多个材质具有不同的着色方程和参数,可以通过使用一个纹理指定表面的哪些区域具有哪些材质来应用于一个表面,从而为每个材质执行不同的代码。例如,有一些生锈区域的金属表面可以使用纹理来指示生锈的位置,根据纹理查找有条件地执行着色器生锈的部分,否则执行闪亮的金属着色器(第9.5.2节)。
着色模型的输入,如表面颜色,与着色器的最终颜色输出有线性关系。因此,包含这些输入的纹理可以用标准技术过滤,避免了混叠。包含非线性阴影输入的纹理,如粗糙度或凹凸贴图(章节6.7),需要更多的注意来避免混叠。考虑到阴影方程的滤波技术可以提高这些纹理的效果。这些技术将在第9.13节中讨论。