第六章 纹理
表面纹理(texture)是指其外观和给人的视觉感受,就像是一幅油画的图案一样。而在计算机图形学中,纹理化则指的是一个过程,即通过使用一些图像、函数或者其他数据,来对每个表面位置的外观表现进行修改。例如:我们可以将一张砖墙的彩色图像应用于由两个三角形组成的矩形上,而不是去精确表现砖墙的几何结构。当观察这个砖墙矩形的时候,对应的彩色图像将会显示在这个矩形所在的位置上,这样可以使得这个矩形看起来很像真实的砖墙。除非相机十分靠近墙壁的话,否则砖墙几何细节的缺乏并不会带来明显的视觉瑕疵。
然而,除了缺乏细致的几何结构之外,一些具有纹理的砖墙也有可能无法令人信服。例如:砖墙的砂浆(砖块和砖块之间粘合物)应当是哑光的(matte),而砖块则应当是有光泽的(glossy),但是观察者会注意到,此时这两种材料表现出的粗糙度(roughness)实际上是相同的。为了产生更加令人信服的视觉表现,我们可以将第二张图像纹理应用到这个表面上,这种纹理并不会改变表面的颜色,而是会根据表面位置来修改墙壁的粗糙度。现在砖块和砂浆从图像纹理中获得了颜色,从新纹理中获得了各自所对应的粗糙度值。
通过上述两张纹理图像,观察者可以看到,现在所有的砖块都是有光泽的,而所有的砂浆都是哑光的。但是现在还有一些问题,那就是每个砖块看起来都非常平整,而真实的砖块表面通常都是坑坑洼洼的,是不规则的。可以通过使用凹凸映射(bump mapping),对砖块表面的着色法线进行一些修改,从而使得其在渲染之后看起来并不平整。这类纹理贴图通过对矩形的原始表面法线进行抖动处理,从而改变光照的计算结果。
如果从一个接近平行的角度来观察这个砖墙的话,那么这种崎岖不平的错觉便会露出马脚,因为现实中的砖块要比砂浆更加突出,因此我们应该是看不到砂浆的。而且即使是从一个垂直的视角进行观察,砖块也应当会在砂浆上投射出阴影。视差映射(parallax mapping)会在渲染平面时,使用一个特殊纹理来使其变形;视差遮蔽映射(parallax occlusion mapping)会对高度纹理进行光线投射,从而提高渲染的真实感。位移映射(displacement mapping)通过使用位移贴图,从而对模型的三角形高度进行修改。图6.1展示了一个具有颜色贴图和凹凸贴图的例子。
图6.1 通过将颜色贴图和凹凸贴图应用到这条鱼身上,从而增加其细节表现。
以上所举的例子都可以通过使用纹理技术来解决,它们使用了越来越复杂的算法。本章节将详细介绍有关纹理处理的相关技术。首先会给出纹理化过程的一般框架。然后将重点关注在纹理表面上应用纹理贴图的过程,因为这是实时渲染中最为流行的纹理使用形式。也会简要讨论有关程序化纹理的内容,也会介绍一些使用纹理贴图来影响表面的常见方法。
纹理管线
纹理化(texturing)是一种用于描述表面材质以及对表面进行修饰加工的有效技术,一种理解纹理的方法是,思考单个着色像素会如何发生变化。正如前一章中所提到的,着色计算需要考虑材质和光源的颜色,以及其他各种复杂的因素;如果场景中还有透明物体的话,那么还需要考虑透明度对着色计算的影响。纹理的工作原理是通过修改着色方程中所使用的参数,从而对最终的着色结果产生影响,而这些参数通常会随着表面位置的变化而变化。例如:对于上文中的砖墙例子,会根据着色点在表面上的位置信息,将该点的颜色替换为砖墙图像中的对应颜色。为了与屏幕上的像素(pixel)概念有所区别,图像纹理中的像素通常被称为纹素(texel)。粗糙度纹理修改了表面的粗糙度值,凹凸纹理修改了表面着色法线的方向,因此每个纹理都对最终着色方程的计算结果产生了影响。
纹理化的过程可以被描述为一个更加一般的纹理管线。稍后将会介绍许多术语,将会详细描述这个纹理管线中的每个部分。
纹理化的起点首先是空间中的一个具体位置,这个位置可以在世界空间中,但是通常都会放在模型的参考坐标系中,因为当模型发生移动的时候,纹理也会随之移动。这里我们使用Kershaw提出的术语:这个空间点会应用一个投影函数(projector function)来获得一组数字,它被称为纹理坐标(texture coordinates),这个纹理坐标将用于访问和采样纹理,这个过程被称为纹理映射(texture mapping)。有时候纹理图像本身会被称为纹理贴图(texture map),尽管这并不是严格正确的。
在使用纹理坐标访问纹理之前,还需要使用一个或者多个转换函数(corresponder function),来将纹理坐标转换到纹理空间中。 转换后的纹理空间位置用于在纹理中获取像素值,例如:纹理空间位置可以是图像纹理中的数组索引,从而检索到对应位置上的像素值。检索到的像素值可能还需要使用一个值转换函数(value transform function)来进行转换,最终这些新值会用于对表面的某些属性进行修改,例如材质或者着色法线等。图6.2详细展示了一个纹理的应用过程。这个纹理管线之所以要设计得如此复杂,是因为其中的每个步骤都可以为用户提供一些有用的控制。需要注意的是,并不是每次纹理应用过程都需要激活管线中的所有步骤。
图6.2 针对单个纹理的广义纹理管线 。
例如:对于一个具有砖墙纹理的三角形,在其表面上进行采样时会发生如下情况(如图6.3所示):首先会在该物体的局部参考系中,找到对应的采样位置 ( x , y , z ) (x, y, z) (x,y,z),这里假设它是 ( − 2.3 , 7.1 , 88.2 ) (−2.3,7.1,88.2) (−2.3,7.1,88.2)。然后会对这个位置坐标应用一个投影函数,就像世界地图是三维地球的二维投影那样,这里的投影函数通常会将一个三维向量 ( x , y , z ) (x, y, z) (x,y,z)转换为一个二维向量 ( u , v ) (u, v) (u,v)。本例中所使用的投影函数,实际上与正交投影是等价的,它就像幻灯片放映机一样,将砖墙图像投影到三角形表面上;并且为了最后能将图象值返回到墙面上,其表面上的点都会被转换为一个0-1范围内的数值对,这里我们假设转换后的值是 ( 0.32 , 0.29 ) (0.32,0.29) (0.32,0.29),这个数值对也被称为纹理坐标或者UV坐标。这个纹理坐标将用于查找纹理贴图在此位置上的颜色值。假设这里所使用的砖墙纹理分辨率为 256 × 256 256\times 256 256×256,因此使用转换函数,将纹理坐标 ( u , v ) (u, v) (u,v)各自乘以256,即 ( 81.92 , 74.24 ) (81.92,74.24) (81.92,74.24)。在丢弃小数部分之后,我们在砖墙图像中进行检索,找到索引值为 ( 81 , 74 ) (81,74) (81,74)的颜色值,这里假设这个颜色值为 ( 0.9 , 0.8 , 0.7 ) (0.9, 0.8, 0.7) (0.9,0.8,0.7)。同时,所使用的纹理颜色位于sRGB颜色空间中,因此如果要在着色方程中使用这个颜色值,还需要将其转换到线性空间中,即 ( 0.787 、 0.604 、 0.448 ) (0.787、0.604、0.448) (0.787、0.604、0.448)。
图6.3 砖墙纹理管线过程中的参数变化。
投影函数
纹理处理的第一步是获取表面的位置,并将其投影到纹理坐标空间(texture coordinate space)中,这个纹理坐标空间通常是一个二维 ( u , v ) (u,v) (u,v)空间。常见的建模软件都允许艺术家定义每个顶点的 ( u , v ) (u,v) (u,v)坐标。这些 ( u , v ) (u,v) (u,v)坐标可以从投影函数(projector function)或者网格展开算法(mesh unwrapping algorithm)中进行初始化,艺术家也可以像编辑顶点位置那样,对 ( u , v ) (u,v) (u,v)坐标进行编辑。投影函数的作用通常是将空间中的三维坐标转换为二维纹理坐标,在建模软件中常用的投影函数包括球面投影(spherical)、柱面投影(cylindrical)和平面投影(planar)等。
投影函数还可以有其他的输入参数,例如:表面法线可以用来选择应用于该表面的平面投影方向(一共六个)。在面片接缝处(即UV接缝)常常会出现纹理匹配的问题,Geiss 讨论了一种将UV接缝混合在一起的技术。Tarini等人描述了立方体映射技术(polycube maps),在该方法中,一个模型会被映射到一组立方体投影上,空间中的不同区域会被映射到不同的立方体上。
而其他的一些投影函数实际上根本就不是投影操作,而是隐含在了表面创建和曲面细分中。例如:参数化曲面的定义本身就包含了一组天生的 ( u , v ) (u,v) (u,v)坐标,如图6.4所示。纹理坐标也可以从其他不同的参数中生成,例如观察方向、表面温度(热力图)或者任何其他可以想象的东西。投影函数的最终目标是生成纹理坐标,将其作为一个与位置有关的函数来进行推导,只是其中的一种方法。
图6.4 不同的纹理投影方法。第一行从左到右分别是:球面、柱面、平面和自然 ( u , v ) (u,v) (u,v)投影。第二行则展示了这些投影方法应用于同一个物体的结果(不包含自然投影)。
非交互式的渲染器会经常调用这些投影函数,这是渲染过程本身的一部分。虽然有时候单个投影函数就可以用于整个模型的投影操作,但是艺术家通常会使用一些工具来将模型进行细分,并单独应用不同的投影函数,如图6.5所示。
图6.5 展示了如何在单个模型上使用不同的纹理投影方法。右图中的box映射由六个平面映射组成,其中每个立方体面都对应一个平面映射。
在实时渲染中,通常会在建模阶段使用投影函数,并将结果数据存储在顶点上。但是情况并非总是如此,有时在顶点着色器或者像素着色器中应用投影函数是会带来一些好处,这样做可以提高精度,并有助于实现包括动画在内的各种效果。有时候一些渲染方法有着自己独特的投影函数,它们会进行逐像素的计算,例如环境映射
球面投影(spherical projection,图6.4左侧)会将表面点投影到一个以某点为中心的假想球体上,Blinn和Newell在其环境映射方案中所使用的投影方法便是球面投影。但是这种投影方法与顶点插值方法,都存在相同的问题。
柱面投影(cylindrical projection)计算纹理坐标 u u u的方法与球面投影相同,而纹理坐标 v v v则是沿圆柱体轴的距离。这种投影方法对于具有中心轴的物体而言十分有用,例如旋转的表面。柱面投影的缺点是,当一个表面几乎垂直于圆柱体的中轴时,就会发生畸变(如柱面投影两端的圆形表面)。
平面投影(planar projection)就像一束 x x x射线一样,它会沿着一个方向进行平行投射,并将纹理应用到所有的表面上。平面投影使用了正交投影方法。这种类型的投影在贴花(decal)应用中十分有用。
与投影方向平齐的表面会发生严重的扭曲,因此艺术家经常需要手动将模型分解成接近平面的小块(即建模流程中的分UV)。有一些工具可以通过对网格进行展开,或者创建一组接近最优的平面投影,来帮助减少这种扭曲现象。目标是让每个多边形在纹理区域中尽量占据更加公平的份额,同时尽可能多的保持网格连通性。网格的连通性是非常重要的,因为在网格的接缝处很容易出现采样瑕疵。一个具有良好展开效果的网格,可以使得艺术家的后续工作变得更加轻松。图6.6中所展示的UV图和纹理图,与图6.5中的雕像相对应。这个展开过程是一个更大的研究领域(网格参数化,mesh parameterization)的其中一部分,有兴趣的读者可以参考Hormann等人的SIGGRAPH课程讲义。
图6.6 几张用于渲染雕像模型的小纹理,它们被打包存储在两个较大的纹理中。右图展示了这个雕像的三角形网格是如何被展开的,以及是如何将纹理映射到三角形网格上的。
纹理坐标空间并不总是一个二维平面,有时候它也可能是一个三维体积,在这种情况下,纹理坐标会被表示为一个包含三个分量的向量 ( u , v , w ) (u, v, w) (u,v,w),其中 w w w是沿着投影方向的深度。有一些系统会使用多达四个坐标,通常是 ( s , t , r , q ) (s, t, r, q) (s,t,r,q),其中 q q q代表了齐次坐标中的第四个值。它的作用类似于电影放映机或者幻灯片投影机,随着投影距离的增加,投影产生的纹理大小也会相应增加。例如,它可以用于在舞台或者其他表面上,投影一个装饰性的聚光灯图案(被称为gobo)。
另一类重要的纹理坐标空间是方向性的,纹理空间中的每个点都需要通过输入方向来进行访问。将这种空间进行可视化的一种方法是,将其作为单位球体上的点,每个点位置上的法线代表了用于访问该位置纹理的输入方向。使用这种方向性参数化的、最常见的纹理类型就是立方体贴图。
值得注意的是,一维的纹理图像和投影函数也有各自的用途。例如:对于一个地形模型而言,它的表面颜色可以由该点对应的高度决定,即低地是绿色的,山峰是白色的。线条同样也可以被纹理化,其中一个应用场景是,将雨渲染为一组带有半透明图像纹理的长线条。这样的纹理也可以用于将一个值转换为另一个值,例如将其作为一个一维查找表。
由于多个纹理可以被应用到同一个表面上,因此可能需要定义多组纹理坐标。但是无论怎样使用这些纹理坐标,其核心思想都是相同的:这些纹理坐标会在表面上进行插值,并用于检索纹理值。然而,在进行插值之前,这些纹理坐标还需要使用转换函数进行变换。
转换函数
转换函数(corresponder function)用于将纹理坐标转换为纹理空间中的具体位置,它们提高了在表面上应用纹理的灵活性。转换函数的其中一个例子是:使用API选择现有纹理中的一部分来进行显示;并且在后续操作中都只会用到这个子图像。
另一类转换函数是矩阵变换,应用于顶点着色器或者像素着色器中,它们允许对表面上的纹理进行平移、旋转、缩放、剪切或者投影操作。正如章节4中所讨论的,这些变换操作的顺序是很重要的。令人惊讶的是,纹理的变换顺序必须与预期的变换顺序相反,这是因为纹理变换实际上是对决定图像可见位置的遮罩空间产生了影响;图像本身并没有被变换,真正发生变换的是定义图像位置的空间。
另一类转换函数控制了图像的应用方式,当纹理坐标 ( u , v ) (u, v) (u,v)在 [ 0 , 1 ] [0,1] [0,1]范围内时,表面上才会出现图像,但是如果纹理坐标位于这个范围之外呢?转换函数决定了此时会发生什么。在OpenGL中,这种类型的转换函数被称为“包装模式(wrapping mode)”;而在DirectX,则被称为“纹理寻址模式(texture addressing mode)”。这种类型的常见转换函数包括:
- wrap(DirectX),repeat(OpenGL)或者tile:图像会在表面上进行重复。在算法实现中,纹理坐标的整数部分会被直接丢弃(这使得纹理坐标在 [ 0 , 1 ] [0,1] [0,1]范围不断重复)。这种模式对于让材质图像在表面上不断重复而言十分有用,并且通常都是默认的模式。
- mirror:图像在表面上不断重复,但是每重复一次就会被镜像(翻转)一次。例如:图像在纹理坐标为0-1之间时表现正常,然后在1-2之间时会进行反转,然后在2-3之间时表现正常,然后再反转,以此类推。这种模式可以为纹理边缘提供一些连续性。
- clamp(DirectX)或者clamp to edge(OpenGL):位于 [ 0 , 1 ] [0,1] [0,1]范围外的纹理坐标会被限制在这个范围内。这种模式会导致图像边缘的不断重复,其的优点在于:当在纹理边缘附近发生双线性插值时,这个模式可以避免从纹理的相反边缘处采样。
- border(DirectX)或者clamp to border(OpenGL):位于 [ 0 , 1 ] [0,1] [0,1]范围外的纹理坐标所采样到的值,会被设定为同一个颜色,它被称为边框颜色(border color),这个边框颜色是可以自己定义的。这种模式可以很好地将贴花渲染到一个单色表面上,因为纹理的边缘颜色会与这个边框颜色进行平滑地混合。
如图6.7所示。每个纹理轴上所分配的转换函数可以是不同的,例如:可以在轴 u u u上使用repeat模式,而在轴 v v v上则使用clamp模式。在DirectX中还有一个mirror once模式,它可以对纹理只进行一次镜像操作,然后再使用clamp模式,这种特殊模式对于对称贴花而言十分有用。
图6.7 纹理图像的不同寻址模式,从左到右分别是repeat,mirror,clamp和border。
纹理的重复平铺是一种为场景添加更多视觉细节的廉价方法,但是通常来说,这种方法在纹理重复三次之后就看起来不太自然了,因为人眼会识别出这种重复的图案。避免这种周期性(periodicity)问题的一个常见方法是,将纹理值与另一个非重复平铺的纹理相结合。这种方法可以被极大扩展,例如Andersson 所描述的商用地形渲染系统,该系统可以根据地形类型、高度、坡度等因素,对多种纹理进行组合。同时纹理图像也会与几何模型(例如灌木和岩石)在场景中的位置有关。
另一个避免周期性问题的方法是,使用着色器程序来实现一些特殊的转换函数,从而将纹理图案和瓦片(tile)贴图进行随机重组。Wang tiles就是这种方法的一个例子,它是一组边缘匹配的方形瓦片贴图,在纹理化过程中,会对集合中的瓦片贴图进行随机选择。Lefebvre和Neyret 实现了一个类似的转换函数,来避免纹理图案的重复问题,它使用了依赖纹理读取和纹理表格。
最后一种被应用的转换函数是隐式的,并且与图像的大小有关。纹理通常会应用在 u v uv uv坐标的 [ 0 , 1 ] [0,1] [0,1]范围内。例如砖墙的例子,通过将该范围内的纹理坐标乘以图像的分辨率,便可以得到对应的像素位置。这种将纹理坐标限制在 [ 0 , 1 ] [0,1] [0,1]范围内的优势在于:可以使用不同分辨率的纹理贴图,而且不需要修改存储在模型顶点中的纹理坐标值。
纹理值
在使用转换函数生成纹理空间坐标之后,便可以使用这个坐标来获取对应的纹理值。对于图像纹理而言,这是通过检索图像中的纹素信息来完成的。在实时渲染中所使用的绝大多数纹理都是图像纹理,但是也可以使用一些函数来程序化生成纹理的内容。在使用程序化纹理的情况下,从纹理空间位置获得纹理值的过程并不涉及内存查找,而是变成了对一个函数进行计算。
最直接的纹理值就是 R G B RGB RGB三元组,它可以用于替换或者修改表面的颜色,当然也可以只返回简单的灰度值。另一种可以返回的数据类型是 R G B α RGB\alpha RGBα,如章节5.5所述。其中的 α \alpha α值(alpha)通常代表了颜色的不透明度,它决定了该颜色对像素的影响程度。纹理贴图中不仅仅可以存储颜色数据,还可以存储任何其他类型的数据,例如表面粗糙度等。
从纹理中返回的值可以在使用之前进行选择性地转换,这些转换一般都是在着色器程序中执行的。一个常见的例子是,将数据从无符号范围(0.0到1.0)重新映射到有符号范围(- 1.0到 1.0)内,它可以用来在纹理贴图中存储法线数据。
图像纹理
在图像纹理化的过程中,二维图像被有效地附着在一个或者多个三角形的表面上。在上一小节中讨论了如何根据纹理坐标来计算对应的纹理空间位置;现在将解决从给定位置的图像纹理中,获取纹理值的相关问题和算法。在本章节的剩余部分中将图像纹理(image texture)将简称为纹理(texture);此外,当提到像素的单元格(pixel’s cell)时,它实际上指的是围绕该像素的屏幕网格单元(screen grid cell)。像素实际上是一个被显示出来的颜色值,它会(而且应该,为了更好的质量)受到与其相关网格单元之外的样本影响。
在本小节中将特别关注于快速采样和过滤纹理图像的方法。章节5讨论了走样和锯齿问题,尤其是在渲染物体边缘的时候;纹理同样也会遇到采样问题,不同的是,它们会出现在被渲染的三角形内部。
像素着色器可以通过将纹理坐标传给texture2D等函数,并调用它们来访问纹理;这些纹理坐标位于 ( u , v ) (u,v) (u,v)纹理坐标系中,会通过转换函数来将其映射到 [ 0.0 , 1.0 ] [0.0,1.0] [0.0,1.0]范围中,然后再由GPU负责将这个值转换为纹素坐标。在不同的图形API中,纹理坐标系统有两个主要的区别。在DirectX中,纹理的左上角对应 ( 0 , 0 ) (0,0) (0,0),右下角是对应 ( 1 , 1 ) (1,1) (1,1),这与大多数图像类型存储数据的方式相匹配,因为位于图像顶部的数据会被存储在图像文件的起始位置;而在OpenGL中,左下角的位置对应了 ( 0 , 0 ) (0,0) (0,0),这刚好是将DirectX的纹理坐标系,按照 y y y轴翻转后的结果。纹素具有整数类型的坐标,但会经常想要访问两个纹素之间的位置,并在它们之间进行插值,这就引出了一个问题:像素中心的浮点坐标到底是什么?Heckbert 讨论了两种可能的模式:截断(truncate)和舍入(round)。DirectX 9将每个纹素的中心定义在 ( 0.0 , 0.0 ) (0.0,0.0) (0.0,0.0)处,它采用了舍入方法。但是这个系统稍微有点混乱,因为对于DirectX左上角像素(原点)而言,该像素的左上角坐标为 ( − 0.5 , − 0.5 ) (−0.5,−0.5) (−0.5,−0.5) 。DirectX 10学习了OpenGL的纹理坐标系统,让每个纹素的中心值为 ( 0.5 , 0.5 ) (0.5,0.5) (0.5,0.5),即使用了截断方法,或者更准确地说是向下取整(floor),即小数部分会被丢弃。向下取整是一个更加直观的系统,它可以很好的用语言进行表述,例如:当说一个像素位于坐标 ( 5 , 9 ) (5,9) (5,9)时,实际上指的是沿 u u u轴方向上从5.0-6.0的范围,以及沿 v v v轴方向上从9.0-10.0的范围。
依赖纹理读取(dependent texture read)是一个值得解释的术语,它包含两个定义。第一个定义是针对移动设备而言的,当使用texture2D或者类似方式访问纹理,并在像素着色器内手动计算纹理坐标,而不是使用从顶点着色器传入的、未修改的纹理坐标时,就会发生依赖纹理读取。这里提到的手动计算纹理坐标,包括任何对输入纹理坐标的修改,甚至是像交换 u u u和 v v v这样的简单操作。对于那些不支持OpenGL ES 3.0的老旧移动GPU而言,在没有依赖纹理读取的情况下会具有更高的效率,因为纹理数据可以被预先读取。另一个定义对于早期的桌面GPU十分重要,在这种情况下,当一个纹理坐标依赖于之前的纹理值结果时,就会发生依赖纹理读取。例如:一个纹理可能会改变表面的着色法线,这反过来又会改变用于访问立方体贴图(cube map)的坐标。这种功能在早期的GPU上是受限的,甚至是不存在的。如今绝大部分GPU都支持了这个功能,但是这样的读取操作可能会对性能产生影响,这取决于在一个batch中计算的像素数量,以及其他的一些因素。
GPU所使用的纹理尺寸通常为 2 m × 2 n 2^m ×2^n 2m×2n ,其中 m m m和 n n n为非负整数,这样的纹理被称为2次幂(power-of-two,POT)纹理。现代GPU可以处理任意大小的非2次幂(non-power-of-two,NPOT)纹理,这允许将生成的图像也视为纹理。但是一些老旧的移动端GPU可能并不支持NPOT纹理的mipmap。不同的图形加速器对于纹理尺寸有着不同的上限,例如:DirectX 12允许一个纹理最多包含 1638 4 2 16384^2 163842个纹素。
假设现在有一个尺寸为 256 × 256 256 × 256 256×256的纹理,并且想将其显示在一个正方形上,只要此时屏幕上投影后正方形尺寸与纹理的尺寸大致相同,那么这个正方形上的纹理看起来就几乎与原始图像一模一样。但是设想一下,如果在投影之后,这个正方形所覆盖的像素尺寸是原始图像的十倍(放大,magnification);又或者在投影之后,正方形只能覆盖屏幕上的一小部分(缩小,minification),这时会发生什么呢?这个问题的答案取决于在这两种不同情况下,使用了什么样的采样方法和过滤方法。
本小节所讨论的图像采样方法和过滤方法,将会应用于每个从纹理读取中的值。然而期望的结果是在最终图像中的避免锯齿和走样的出现,在理论上,这需要对最终的像素颜色进行采样和过滤。这里二者的区别在于,是对着色方程的输入进行过滤,还是对着色方程的输出进行过滤。实际上,只要着色方程的输入和输出是线性相关的(例如颜色),那么对单个纹理值的过滤与对最终颜色的过滤其实是等效的。然而,许多存储在纹理中的着色器输入参数,例如表面法线和粗糙度,与着色器的输出之间具有非线性关系,标准的纹理过滤方法不能很好地处理这些纹理,最终会导致出现走样。
放大
在图6.8中,一个尺寸为 48 × 48 48 × 48 48×48的纹理被纹理化到了一个正方形表面上,相对于纹理尺寸而言,这个正方形到相机的距离非常近,因此底层的图形系统需要将这个纹理进行放大(magnification)。最常见的放大过滤技术是邻近过滤(nearest neighbor,实际使用了box滤波器,详见章节5)和双线性插值(bilinear interpolation)。还有一种方法叫做三次卷积插值(cubic convolution),它使用了 4 × 4 4 × 4 4×4或者 5 × 5 5 × 5 5×5范围纹素的加权和,它可以实现更高质量的放大。虽然现在的硬件并不原生支持三次卷积插值(也被称为双三次插值,bicubic interpolation),但是它可以在着色器程序中进行执行。
图6.8 将 48 × 48 48 \times 48 48×48的纹理放大到 320 × 320 320 \times 320 320×320。左:邻近过滤,每个像素都会选择距离其最近的纹素。中间:双线性过滤,使用四个最近像素的加权平均值。右:立方滤波(三次滤波),使用 5 × 5 5 \times 5 5×5范围内最近像素的加权平均值。
在图6.8的最左侧使用了邻近过滤的方法。这种放大技术的一个特点是,单个纹素可能会变得十分明显。这种效果被称为像素化(pixelation);因为该方法在放大的时候,会选取距离每个像素中心最近的纹素,从而产生了块状外观。虽然这种方法的质量有时会很差,但是它的好处在于,只需要为每个像素获取一个纹素即可。
在图6.8的中间使用了双线性插值(有时也会叫做线性插值)方法。对于每个像素而言,这种过滤方法需要找到四个相邻的纹素,并在二维上进行线性插值,从而获得混合后的像素值。虽然双线性插值的结果是比较模糊的,但是它并不会像邻近过滤那样出现锯齿。你可以做一个简单的小实验,尝试眯着眼睛来看左边的图像,你就会发现图像的锯齿也消失了,因为这样做(眯着眼睛观察)的效果其实和低通滤波器是大致相同的,并且更能展示面部的特征。
这里回到本章一开始提到的砖块纹理例子:在不舍弃小数的情况下会获得坐标 ( p u , p v ) = ( 81.92 , 74.24 ) (p_u, p_v) =(81.92, 74.24) (pu,pv)=(81.92,74.24)。这里使用与OpenGL同样的纹理坐标系,其原点位于左下角,它与标准的笛卡尔坐标系是相匹配的。目标是在四个最近的纹素中心之间,建立一个局部坐标系,并在这四个纹素中心之间进行插值,最终获得该点的像素值,如图6.9所示。为了找到4个最近的相邻像素,从采样位置减去像素中心的分数部分 ( 0.5 , 0.5 ) (0.5,0.5) (0.5,0.5),得到 ( 81.42 , 73.74 ) (81.42,73.74) (81.42,73.74)。在去掉中心的小数部分之后,距离最近的4个像素范围即为 ( x , y ) = ( 81 , 73 ) (x, y) =(81,73) (x,y)=(81,73)到 ( x + 1 , y + 1 ) = ( 82 , 74 ) (x+1, y+1) =(82,74) (x+1,y+1)=(82,74)。在这个例子中,分数部分 ( 0.42 , 0.74 ) (0.42,0.74) (0.42,0.74)是该采样点在这个局部坐标系(由相邻的四个纹素中心构成)中的位置,将这个位置表示为 ( u ′ , v ′ ) \left(u^{\prime}, v^{\prime}\right) (u′,v′)。
图6.9 双线性插值。涉及的四个纹素由左边的四个正方形表示,其中蓝点代表了纹素的中心点。右边则展示了由四个纹素中心所形成的局部坐标系。
这里将纹理访问函数定义为 t ( x , y ) t(x, y) t(x,y),该函数会返回对应纹素的颜色,其中 x x x和 y y y是整数。那么任意位置 ( u ′ , v ′ ) \left(u^{\prime}, v^{\prime}\right) (u′,v′)的双线性插值颜色可以按照以下两步进行计算:首先,使用下方的两个纹素颜色 t ( x , y ) t (x, y) t(x,y)和 t ( x + 1 , y ) t (x + 1, y) t(x+1,y),按照参数 u ′ u^{\prime} u′进行插值,即 ( 1 − u ′ ) t ( x , y ) + u ′ t ( x + 1 , y ) (1−u^{\prime})t(x, y) + u^{\prime}t (x + 1, y) (1−u′)t(x,y)+u′t(x+1,y);再使用上方的两个纹素颜色 t ( x , y + 1 ) t (x, y + 1) t(x,y+1)和 t ( x + 1 , y + 1 ) t (x + 1, y+ 1) t(x+1,y+1),按照参数 u ′ u^{\prime} u′进行插值,即 ( 1 − u ′ ) t ( x , y + 1 ) + u ′ t ( x + 1 , y + 1 ) (1−u^{\prime}) t (x, y + 1) +u^{\prime}t (x + 1, y+ 1) (1−u′)t(x,y+1)+u′t(x+1,y+1),如图6.9左侧的绿色圆圈。然后对这两个值在竖直方向上,按照参数 v ′ v^{\prime} v′进行插值,即将上述过程结合起来,最终 ( p u , p v ) (p_u, p_v) (pu,pv)处的双线性插值颜色 b \mathbf{b} b为:
b ( p u , p v ) = ( 1 − v ′ ) ( ( 1 − u ′ ) t ( x , y ) + u ′ t ( x + 1 , y ) ) + v ′ ( ( 1 − u ′ ) t ( x , y + 1 ) + u ′ t ( x + 1 , y + 1 ) ) = ( 1 − u ′ ) ( 1 − v ′ ) t ( x , y ) + u ′ ( 1 − v ′ ) t ( x + 1 , y ) + ( 1 − u ′ ) v ′ t ( x , y + 1 ) + u ′ v ′ t ( x + 1 , y + 1 ) . (6.1) \begin{aligned} \mathbf{b}\left(p_{u}, p_{v}\right)= & \left(1-v^{\prime}\right)\left(\left(1-u^{\prime}\right) \mathbf{t}(x, y)+u^{\prime} \mathbf{t}(x+1, y)\right) \\ & +v^{\prime}\left(\left(1-u^{\prime}\right) \mathbf{t}(x, y+1)+u^{\prime} \mathbf{t}(x+1, y+1)\right) \\ = & \left(1-u^{\prime}\right)\left(1-v^{\prime}\right) \mathbf{t}(x, y)+u^{\prime}\left(1-v^{\prime}\right) \mathbf{t}(x+1, y) \\ & +\left(1-u^{\prime}\right) v^{\prime} \mathbf{t}(x, y+1)+u^{\prime} v^{\prime} \mathbf{t}(x+1, y+1) .\end{aligned} \tag{6.1} b(pu,pv)==(1−v′)((1−u′)t(x,y)+u′t(x+1,y))+v′((1−u′)t(x,y+1)+u′t(x+1,y+1))(1−u′)(1−v′)t(x,y)+u′(1−v′)t(x+1,y)+(1−u′)v′t(x,y+1)+u′v′t(x+1,y+1).(6.1)
从直观上来说,距离采样位置越近的纹素,对其最终颜色值的影响也就越大,这也是在方程6.1中所描述的。右上角 ( x + 1 , y + 1 ) (x+ 1, y + 1) (x+1,y+1)处的纹素对其的影响是 u ′ v ′ u^{\prime}v^{\prime} u′v′。注意这个等价关系:右上角纹素的影响力,等于左下角纹素与采样点之间所形成的矩形面积。回到例子中,这意味着从该纹素上检索到的像素值,会被乘以 0.42 × 0.74 0.42 × 0.74 0.42×0.74,即 0.3108 0.3108 0.3108。从右上角纹素开始,按照顺时针顺序,其他三个纹素会被分别乘以 0.42 × 0.26 0.42 × 0.26 0.42×0.26, 0.58 × 0.26 0.58 × 0.26 0.58×0.26和 0.58 × 0.74 0.58 × 0.74 0.58×0.74,这四个权重的和为 1.0 1.0 1.0。
一种用于解决放大模糊的常见方法是使用细节贴图(detail texture)。这个纹理代表了表面上的精细细节,例如手机上的划痕和地形上的灌木丛等。这些细节会作为一个独立的纹理贴图,以不同的尺度被覆盖在放大后的纹理上。细节贴图中包含了高频的重复图案,它与低频的放大纹理相结合,在视觉效果上类似于使用单张高分辨率的纹理。
双线性插值会在两个方向上进行线性插值,但是有些情况下其实并不需要线性插值。例如:一个纹理是由类似于棋盘格的黑白像素组成的,使用双线性插值使得原本的黑白结果变成了平滑的灰度值。这时候需要对结果进行通过重新映射,例如可以让所有低于0.4的灰色都变成黑色,所有高于0.6的灰色都变成白色,而那些介于两者之间的灰色,则被拉伸用以填补空白,使得纹理看起来更像是原本的棋盘格,同时也在纹理之间进行了一些混合过渡,如图6.10所示。
图6.10 从左到右分别是邻近过滤,双线性插值,和部分重新映射的结果,原本的纹理是一个 2 × 2 2 × 2 2×2的黑白棋盘格。需要注意的是,由于纹理和图像网格并不是完美匹配的,因此邻近过滤方法所生成的正方形大小略有不同。
使用一个更高分辨率的纹理也会出现类似重新映射的效果。例如:假设现在每个黑白方格都由 4 × 4 4 × 4 4×4,总计16个像素组成(原本是 1 × 1 1 × 1 1×1),那么在每个方格的中心周围,插值出来的颜色也将是全黑或者全白的。
在图6.8的右侧,使用了双三次插值(bicubic filter),它大幅去除了剩余的方块感。需要注意的是,双三次插值比双线性插值的计算成本更高,但是许多的高阶滤波器都可以被表示为重复的线性插值[1518],因此可以通过若干次简单的线性插值,来充分利用纹理单元中用于线性插值操作的GPU硬件。
如果觉得双三次插值的计算成本太大的话,那Quilez提出了一种更加简单的技术,它在一组 2 × 2 2 × 2 2×2纹素之间,使用了一个平滑的曲线来进行插值。接下来首先会先介绍这个平滑曲线,然后再描述这个技术的详细过程。最常用的两个平滑曲线分别是smoothstep曲线和quintic(五次)曲线,它们的数学表达如下:
s ( x ) = x 2 ( 3 − 2 x ) ⏟ smoothstep a n d q ( x ) = x 3 ( 6 x 2 − 15 x + 10 ) ⏟ quintic (6.2) \underbrace{s(x)=x^{2}(3-2 x)}_{\text {smoothstep }} and \underbrace{q(x)=x^{3}\left(6 x^{2}-15 x+10\right)}_{\text {quintic }} \tag{6.2} smoothstep s(x)=x2(3−2x)andquintic q(x)=x3(6x2−15x+10)(6.2)
当想从一个值平滑插值到另外一个值时,这些函数是非常有用的。smoothstep曲线具有 s ′ ( 0 ) = s ′ ( 1 ) = 0 s^{\prime}(0) = s^{\prime}(1) = 0 s′(0)=s′(1)=0的性质( C 1 C^1 C1连续),它在0-1之间是非常平滑的。quintic曲线具有类似的性质,唯一不同是 q ′ ′ ( 0 ) = q ′ ′ ( 1 ) = 0 q^{\prime \prime}(0) = q^{\prime \prime}(1) = 0 q′′(0)=q′′(1)=0( C 2 C^2 C2连续),即曲线在开始和结束处的二阶导数也是0。这两条曲线如图6.11所示。
图6.11 smoothstep曲线 s ( x ) s(x) s(x)(左侧)和quintic曲线 q ( x ) q(x) q(x)(右侧)。
该方法首先会计算出位置 ( u ′ , v ′ ) \left(u^{\prime}, v^{\prime}\right) (u′,v′)(与方程6.1和图6.9中使用相同的方法),然后将采样位置与纹理尺寸相乘并加上 0.5 0.5 0.5。结果的整数部分暂时先保留,小数部分会存储在 u ′ u^{\prime} u′和 v ′ v^{\prime} v′中,它们的取值范围是 [ 0 , 1 ] [0,1] [0,1]。然后再将 ( u ′ , v ′ ) \left(u^{\prime}, v^{\prime}\right) (u′,v′)变换为 ( t u , t v ) = ( q ( u ′ ) , q ( v ′ ) ) \left(t_{u}, t_{v}\right)=\left(q\left(u^{\prime}\right), q\left(v^{\prime}\right)\right) (tu,tv)=(q(u′),q(v′)),这个结果仍然在 [ 0 , 1 ] [0,1] [0,1]的范围内;将这个结果减去0.5,再加上原来的整数部分。然后将得到的坐标 u u u和坐标 v v v分别除以纹理宽度和纹理高度。此时,会将这个新的纹理坐标作为参数,传给GPU提供的双线性插值查找函数。需要注意的是,这种方法在每个纹素上只会给出一个值,这意味着如果纹素位于RGB空间平面上的话,那么这种类型的插值将给出一个平滑的,但仍然是阶梯状的外观,这可能并不总是想要的,如图6.12所示。
图6.12 放大一维纹理的四种不同方式。其中橙色圆圈代表了纹素中心以及对应纹理值(高度)。从左到右分别是:邻近过滤、线性插值、在相邻纹素之间使用quintic曲线、三次插值。
缩小
当纹理被压缩时,平面上的一个像素单元格可能会占据好几个纹素,如图6.13所示。为了正确获得这个像素的颜色值,应当将这几个纹素对像素的影响整合起来。然而,精确确定某个像素附近所有纹素对其的影响是很难的,而且想要以实时的速度来完美地实现这一点几乎是不可能的。
图6.13 缩小(minification):通过一排像素来观察一个具有棋盘格纹理的正方形,大致显示了每个像素是如何被多个纹素所影响的。
由于上述的一些限制,因此在GPU上使用了一些特殊方法来解决这个问题。其中最简单的方法便是使用邻近过滤(nearest neighbor),其工作原理与用于纹理放大的滤波器完全相同,即直接选择位于像素单元中心可见的纹素值,来作为自身的像素值,但是这个方法可能会产生严重的锯齿问题,如图6.14第一行的图片。因为这种方法只是在影响像素的众多纹素中,选择一个纹素来代表该点的颜色,当表面相对于相机发生移动的时候,这种瑕疵会变得更加明显,这种在运动中所产生的瑕疵也被称为时域锯齿(temporal aliasing)。
图6.14 第一行图像使用了点采样(邻近过滤),第二行图像使用了mipmap,第三行图像使用了SAT。
另一个常用的滤波器是双线性插值,其工作原理与上文中的放大滤波器完全相同;但是对于纹理缩小而言,这种方法只比邻近过滤稍微好一点,因为它只能将四个纹素对于像素的影响混合起来;而当一个像素受到超过四个纹素的影响时,双线性插值就会很快失效并产生锯齿。
还有一些更好的解决方法,正如章节5中所讨论的,走样(锯齿)问题可以通过采样技术和滤波技术来解决。纹理的信号频率取决于纹素在屏幕上的间隔距离,根据Nyquist极限,只需要确保纹理的信号频率不大于采样频率的一半即可。例如:假设现在有这样一个图像,它由黑白相间的线条组成,其中每个线条占据了两个纹素,即频率为 1 2 \frac{1}{2} 21。为了在屏幕上能够正确显示这个纹理,那么采样频率必须至少为 2 × 1 2 = 1 2 \times\frac{1}{2}=1 2×21=1,即至少一个像素。因此对于纹理而言,一般一个像素应当最多对应一个纹素,这样可以避免走样和锯齿。
根据上面的分析,为了实现这个目标,要么提高像素的采样频率,要么降低纹理的信号频率。上一章节中所讨论的抗锯齿方法,主要是通过提高像素采样率来实现的;但是这些方法能够增加的采样率是有限的。为了更加充分地解决这个问题,需要降低纹理的频率,因此提出了各种纹理缩小算法。
所有纹理抗锯齿算法背后的基本思想都是相同的:对纹理进行预处理,创建某种数据结构,从而快速近似计算一组纹素对像素的影响。对于实时渲染而言,这些算法在执行过程中具有使用固定时间开销和资源开销的特点。通过这种方式,每个像素会采集固定数量的样本,并将这些结果组合起来,从而计算大量纹素对像素的影响。
Minimap
mipmap 是最流行的纹理抗锯齿方法,现如今所有的图形加速器都会支持这种方法。其中Mip是拉丁语(multum in parvo)的缩写,它的意思是“一个很小的地方上有很多东西”,这是一个很好的名字,因为mipmap的过程其实就是将原始纹理反复过滤成更小的图像。
在使用mipmap滤波器的时候,在实际渲染发生之前,原始纹理图像会生成一系列较小尺寸的版本。原始纹理(第0级)会被下采样到原始尺寸的四分之一,每个新生成的纹素值,通常为原始纹理中四个相邻纹素的平均值,这个新生成的纹理(第1级)有时也会被叫做原始纹理的子纹理(subtexture)。这个下采样的过程会被递归执行,直到最终生成的某个纹理的维度为1。这个过程如图6.15所示,这组图片的集合通常被称为一个mipmap链(mipmap chain)。
图6.15 金字塔的底部是原始纹理图像,它对应了mipmap的第0级,将每个 2 × 2 2 × 2 2×2区域内纹素的平均值作为下一级别的mipmap。纵轴是第三个纹理坐标 d d d,在这个图中, d d d并不是线性的,它用于在两个纹理级别之间进行插值。
生成高质量mipmap的两个重要因素分别是:使用良好的过滤和伽马校正。生成mipmap的常用方法是将每 2 × 2 2 × 2 2×2的纹素进行平均,从而获得下一级mip所对应的纹素值。具体使用的是一个box滤波器,虽然这可能是最糟糕的一个滤波器,使用box滤波器可能会导致较差的质量,因为它会对低频信息进行模糊,同时保留一些会产生锯齿的高频信息。最好是使用高斯、Lanczos、Kaiser或者类似的滤波器,这些滤波器的源代码基本都有免费高效的开源实现,同时有一些API还支持在GPU上进行过滤操作。在靠近纹理边缘进行过滤的时候的地方,需要注意纹理的包装模式(wrapping mode)。
对于在非线性颜色空间中进行编码的纹理(例如大多数的彩色纹理),在过滤时忽略伽玛校正会修改该层级mipmap的感知亮度。如果使用了未校正的mipmap,相机距离物体越远,物体整体看起来就会越暗,对比度和表面细节也会受到影响。由于这个原因,因此将这种纹理(例如颜色纹理)从sRGB颜色空间转换到线性颜色空间是十分重要的(章节5),在线性空间中完成mipmap的生成和过滤,然后将生产的结果转换回sRGB颜色空间中并进行存储。大多数图形API都支持sRGB纹理,因此可以在线性空间中正确生成mipmap,并将结果存储在sRGB中。当访问sRGB纹理的时候,它们首先会被转换到线性空间中,以便正确地执行放大(magnification)和缩小(minification)操作。
之前提到,一些纹理参数与最终的着色颜色之间具有非线性关系,这会给过滤带来一些问题,而mipmap的生成对这个问题特别敏感,因为在这个过程中,需要对上百或者上千个像素进行过滤。为了获得最佳的结果,通常需要一些专门的mipmap生成方法。
图6.16 左边是一个正方形的像素单元格及其它的纹理视图。右边是这个像素单元格在纹理上的投影。
在纹理化时访问mipmap的过程也很简单。由于屏幕像素占据了纹理中的一个区域,因此当某个像素被投影到纹理上时(如图6.16所示),它会包含一个或者多个纹素。这里使用像素单元格的边界并不是严格正确的,使用这种方式只是为了简化表示。位于单元格外的纹素也会对像素的颜色产生影响,这里的目标是大致确定纹素对像素的影响程度。对于第三个纹理坐标 d d d(在OpenGL称之为 λ \lambda λ,也称为纹理细节级别,texture level of detail),有两种常用的计算方法:一种是利用像素单元格投影后所形成的四边形,取其中较长的那个边来对像素的覆盖范围进行近似;另一种方法是使用四个梯度中绝对值最大的那个作为度量,这四个梯度分别是 ∂ u / ∂ x , ∂ v / ∂ x , ∂ u / ∂ y , ∂ v / ∂ y \partial u / \partial x, \partial v / \partial x,\partial u / \partial y,\partial v / \partial y ∂u/∂x,∂v/∂x,∂u/∂y,∂v/∂y,它们代表了纹理坐标相对于屏幕轴向的变化量,例如 ∂ u / ∂ x \partial u / \partial x ∂u/∂x代表了一个像素所对应的纹理值 u u u,沿着屏幕 x x x轴的变化量。你可以在Williams 、Flavell 或者Pharr 的论文中了解更多有关这些方程的内容。McCormack等人讨论了采用最大绝对值方法所引入的走样,并提出了一个替代方程。Ewins等人对几种质量相当的算法,以及它们所对应的硬件成本进行了分析。
在shader Model 3.0或者更新版本的像素着色器中,可以直接使用这些梯度值。由于这些梯度基于相邻像素值之间的差异,如果像素着色器中包含了受动态流程控制(章节3)的部分,那么这部分是无法访问这些梯度信息的。如果想要在这部分(例如一个循环)中读取纹理的话,那么就必须提前计算梯度。需要注意的是,顶点着色器并不能访问梯度信息,当需要顶点纹理化操作的时候,才会在顶点着色器中计算梯度或者细节级别等信息,并提供给GPU,以供后续阶段使用。
计算第三个纹理坐标 d d d的目的是确定沿着mipmap金字塔轴进行采样的层级,如图6.15所示。目标是使得像素与纹素的比例至少为 1 : 1 1:1 1:1,以达到Nyquist极限。这里计算坐标 d d d的重要原则是,当一个像素单元格内包含多个纹素时,就需要增大 d d d,从而访问尺寸更小,更模糊的mipmap层级。使用三元组 ( u , v , d ) (u, v, d) (u,v,d)来访问纹理的mipmap,其中 d d d类似于纹理级别,但 d d d并不是一个整数值,而是级别之间距离的分数值。对 d d d两侧的miamap分别进行采样,即使用坐标 ( u , v ) (u, v) (u,v)来从这两个mipmap中分别进行双线性插值,获得两个纹理值。最后按照参数 d d d再对这两个纹理值进行一次线性插值。整个过程被称为三线性插值(trilinear interpolation),并且会逐像素地执行。
用户可以通过细节层次偏移(level of detail bias,LOD bias),来对坐标 d d d进行一定的控制,这是一个加在坐标 d d d上的偏移量,它影响了纹理的相对感知锐度。如果将mipmap金字塔向上移动(即增大 d d d),那么纹理会看起来更加模糊。对于任何给定的纹理,根据图像类型和使用方式的不同,良好的LOD偏移也是不同的。例如:对于有些模糊的第0级mipmap图像,可以使用一个负偏移量;而对于过滤不良(产生锯齿)的纹理图像则可以使用一个正偏移量。这个偏移量可以针对整个纹理进行指定,也可以在像素着色器中逐像素指定。为了获得更加精细的控制,可以由用户来提供坐标 d d d,或者是提供用于计算它的梯度。
mipmap的好处在于,它并不是去单独计算每个纹素对像素的影响,而是对预先生成的纹素集合进行访问和插值,无论纹理压缩的程度如何,这个过程的时间开销是固定的。然而,mipmap也存在几个缺陷,其中一个主要的问题就是过度模糊(overblurring)。假设现在有一个像素单元格,它在 u u u方向上覆盖了大量的纹素,而在 v v v方向上只覆盖了少量的纹素,这种情况通常发生在相机以一个掠射角度来观察纹理表面的时候。在这种情况下,需要沿着纹理的其中一个轴进行缩小,沿着另一个轴进行放大,这会导致像素在纹理上的投影区域是一个长宽比很大的矩形;而在访问mipmap时,只能检索纹理上的正方形投影区域,无法检索矩形投影区域。为了避免走样会选择较长的那个边所形成的正方形,来作为对像素单元格覆盖率的近似度量,这导致检索到的样本往往会相对模糊。这种现象可以在图6.14的mipmap图像中看到,图片右侧向远处延申的线条会变得过度模糊。
Summed-Area表(SAT)
另一种能够避免过度模糊的方法是面积积分表(summed-area table,SAT,也可以叫做求和面积表),后文会简称为SAT。想要使用这种方法,首先要创建一个尺寸与纹理相同的数组,但是颜色存储的精度要更高(例如:每个红绿蓝颜色分量都会占据16个bit)。
图6.17 像素单元格被反向投影到纹理上,并被一个轴对齐矩形包围盒所包围,这个包围盒的四个角会用于访问SAT。
在数组中的每个位置上,该位置上的纹素会和 ( 0 , 0 ) (0,0) (0,0)处的纹素(原点)构成一个矩形,计算并存储区域中所有纹素值的总和。在纹理化的过程中,屏幕上像素在纹理上的投影区域是一个矩形;然后会通过SAT来确定这个矩形区域的平均颜色,并将其作为该像素的纹理颜色。这个计算过程如图6.17所示,具体的平均颜色计算公式如下:
c = s [ x u r , y u r ] − s [ x u r , y l l ] − s [ x l l , y u r ] + s [ x l l , y l l ] ( x u r − x l l ) ( y u r − y l l ) (6.3) \mathbf{c}=\frac{\mathbf{s}\left[x_{u r}, y_{u r}\right]-\mathbf{s}\left[x_{u r}, y_{l l}\right]-\mathbf{s}\left[x_{l l}, y_{u r}\right]+\mathbf{s}\left[x_{l l}, y_{l l}\right]}{\left(x_{u r}-x_{l l}\right)\left(y_{u r}-y_{l l}\right)} \tag{6.3} c=(xur−xll)(yur−yll)s[xur,yur]−s[xur,yll]−s[xll,yur]+s[xll,yll](6.3)
其中的 x x x和 y y y代表了矩形的纹理坐标, s [ x , y ] \mathbf{s}\left[x, y\right] s[x,y]代表了该坐标所对应的SAT值。这个方程的原理是:首先获取右上角到原点这个大矩形的SAT值,然后再根据相邻矩形顶点,获得两个小矩形 A A A和小矩形 B B B的SAT值,并将它们减去;其中右下角区域 C C C的SAT值被减去了两次,因此最后还要再加上一个区域 C C C的SAT值。请注意,坐标 ( x l l , y l l ) (x_{l l}, y_{l l}) (xll,yll)位于区域 C C C的右上角,即坐标 ( x l l + 1 , y l l + 1 ) (x_{l l}+1, y_{l l}+1) (xll+1,yll+1)是包围盒的左下角。
图6.14的第三行使用了SAT方法,图像右侧向远处地平线延申的线条变得更加清晰了,但是中间对角相交的线条仍然是很模糊的。这个问题的原因在于,当沿着对角线观察纹理的时候,像素投影所生成的区域是一个沿对角线的细长矩形,该矩形对应的包围盒中包含了大量无关的纹素,例如:在图6.17中,想象此时像素的投影区域是一个横跨纹理对角线的细长区域,它所对应的包围盒几乎会占据整个纹理,而真正位于像素投影区域内的纹素数量则很少。此时这种方法会对整个纹理矩形进行平均,这个结果包含了大量的无关纹素值,从而导致模糊的产生。
SAT是各向异性过滤(anisotropic filtering)算法的其中一个例子,这类算法用于检索非正方形投影区域的纹理值,SAT对于接近水平方向或者竖直方向的投影区域最为有效。还需要注意的是,对于 16 × 16 16 × 16 16×16或者尺寸更小的纹理,SAT需要至少两倍的内存;而对于尺寸更大的纹理,则需要更高的存储精度,因为像素值的和会很大,精度过低可能会导致数值溢出。
SAT可以提供更好的质量,并且额外的内存开销还算合理,因此它在现代的GPU上也被广泛应用。高质量的过滤方法对于高级渲染技术的质量而言至关重要。例如,Hensley等人提出了一个高效的实现,并展示了使用SAT采样来改善glossy反射的方法。其他使用区域采样的算法也可以通过SAT方法进行改进,例如如景深,阴影贴图,和模糊反射等。
无约束的各向异性过滤
对于目前的图形硬件而言,想要进一步改进纹理过滤的质量,最常见的方法就是重用现有的mipmap硬件。其基本思想是将像素单元格反向投影到纹理上,然后再对纹理上的四边形区域进行多次采样,最后将采样的结果整合在一起,作为该像素的颜色。在之前所提到的方法中,会在mipmap中的一个正方形区域内进行采样,这可能会导致采样到很多无关纹素,使得表面变得模糊。这里将要介绍的算法,并不是使用单个mipmap采样区域来对该投影形成四边形进行近似,而是会使用多个正方形来进行近似。使用四边形中较短的那个边来确定 d d d的值(而在原始的mipmap中,通常会使用较长的边来确定 d d d),这样做会使得每个mipmap样本的平均面积更小(包含了更少的无关像素,因此会减少模糊的出现)。而四边形的长边则被用来创建一条与其平行,并且穿过四边形中点的各向异性线(line of anisotropy)。当各向异性的比例在 1 : 1 1:1 1:1和 2 : 1 2:1 2:1之间时,会沿着这条线取两个样本(如图6.18所示);各向异性的比例越高,沿轴采集的样本就越多。
图6.18 各向异性过滤。像素单元格的反向投影会形成一个四边形,在较长的边之间构建一条各向异性线。
这种方法对各向异性线的方向没有要求,因此它并不会出现类似SAT那样的限制。而且它也不需要比mipmap使用更多的纹理内存,因为它只是在mipmap算法的基础上,对采样方法进行了改进而已,图6.19展示了一个各向异性过滤的例子。
图6.19 mipmap和各向异性之间的对比。左侧使用了三线性插值的mipmap,右侧使用了16:1的各项异性mipmap。在向地平线延申的方向上,各向异性过滤可以提供更加清晰的结果与最少的锯齿。
这种沿轴采样的想法最初是由Schilling等人提出的,并应用在了他们的Texram动态存储设备中。Barkans描述了该算法在Talisman系统中的应用;McCormack等人提出了一个名为Feline的类似系统。Texram的原始方法是,让沿着各向异性轴的采样点 (也称为探针)具有相同的权重,而Talisman系统则只为轴两端的两个探针赋予一半的权重;Feline系统使用一个高斯滤波核来对一组探针进行加权。这些算法的质量接近于软件采样算法,例如椭圆加权平均(Elliptical Weighted Average,EWA)滤波器,这个滤波器会将像素的影响区域,转换为纹理上的一个椭圆区域,并通过滤波核来对椭圆内的纹素进行加权。Mavridis和Papaioannou提出了几种可以在GPU上,使用着色器代码实现EWA过滤的方法。
体积纹理
对图像纹理直接进行扩展可以得到三维图像数据,它通过坐标 ( u , v , w ) (u, v, w) (u,v,w)或者 ( s , t , r ) (s, t, r) (s,t,r)来进行访问,例如:医学成像数据可以生成三维网格,通过在网格中移动成像平面,可以看到这些数据的二维切片。一个类似的想法是,使用这种数据形式来表示体积光,通过在体积内部找到该位置所对应的值,并结合光照方向,可以计算出表面上一点的光照。
如今大部分GPU都支持体积纹理(volume texture)的mipmap,由于在体积纹理的单个mipmap级别内,需要使用三线性插值来进行过滤,因此在不同mipmap级别之间,需要四线性插值(quadrilinear interpolation)来进行过滤。由于需要对16个纹素的结果进行求平均,因此可能会导致一些精度不足的问题,这可以通过使用更高精度的体积纹理来进行解决。Sigg和Hadwiger 讨论了这个问题以及其他与体积纹理相关的问题,并提供了用于过滤和其他操作的高效方法。
虽然体积纹理对于存储空间的要求比较高,并且过滤的计算成本也比较高,但它确实具有一些特殊的优势。由于可以直接使用纹理坐标来表示三维的空间位置,因此可以跳过为三维网格寻找一个良好二维参数化表示的复杂过程(UV拆分)。这避免了二维参数化时经常出现的扭曲和接缝问题。体积纹理也可以用来表示木材或者大理石等材质的体积结构,具有这种纹理的模型,看起来就像是使用这种材料雕刻出来的一样。
使用体积纹理来对表面进行纹理化操作是非常低效的,因为体积纹理中的绝大部分样本都没有被使用。Benson和Davis以及DeBry等人,讨论了将纹理数据存储在稀疏八叉树中的方法,这种方法非常适合交互式的三维绘画系统,因为在创建表面的时候,不需要显式地指定它的纹理坐标,同时八叉树结构可以将纹理细节保留到任何我们想要的级别。Lefebvre等人讨论了在现代GPU上实现八叉树纹理的细节;Lefebvre和Hoppe提出了一种将稀疏体积数据打包成较小纹理的方法。
立方体贴图
另一种类型的纹理叫做立方体纹理(cube texture)或者立方体贴图(cube map),它具有六个正方形的纹理,立方体的六个面分别对应了这个六个正方形纹理。访问立方体贴图需要使用一个包含三个分量的纹理坐标向量,这个向量代表了从立方体中心向外发射的射线方向。这个射线与立方体交点的计算过程如下:向量中绝对值最大的那个分量,决定了射线会射向哪个立方体表面(例如:向量 ( − 3.2 , 5.1 , − 8.4 ) (- 3.2,5.1,−8.4) (−3.2,5.1,−8.4)代表了射线会射向 − z −z −z面)。将剩余的两个坐标分量分别除以最大分量的绝对值(即 8.4 8.4 8.4),此时这两个分量的大小位于 [ − 1 , 1 ] [-1,1] [−1,1]内,然后再将其重新映射到 [ 0 , 1 ] [0,1] [0,1]中以计算纹理坐标,例如:坐标 ( − 3.2 , 5.1 ) (−3.2,5.1) (−3.2,5.1)会被映射为 ( ( − 3.2 / 8.4 + 1 ) / 2 , ( 5.1 / 8.4 + 1 ) / 2 ) ≈ ( 0.31 , 0.80 ) ((−3.2/8.4 + 1)/2,(5.1/8.4 + 1)/2)≈(0.31,0.80) ((−3.2/8.4+1)/2,(5.1/8.4+1)/2)≈(0.31,0.80)。立方体贴图对于表示方向函数的值而言非常有用;它们最常用于环境映射中。
纹理表示
在应用程序中处理大量纹理的时候,有一些方法可以提高性能表现,而本小节的重点则是纹理图集(texture atlas),纹理数组(texture array)以及无绑定的纹理(bindless textures),所有这些技术的目的都是为了在渲染过程中避免纹理的切换,因为切换纹理是有一些额外开销的。
图6.20 左图:纹理图集,其中9个较小的图像被组合为一个大纹理。右图:一种更加现代的方法是将较小的图像设置为纹理数组,大多数现代API中都有类似的概念。
为了能够使得GPU批量处理尽可能多的任务,一般来说最好尽可能地避免改变它的状态。为此,可以将多个图像放入一个尺寸更大的纹理中,这个纹理被叫做纹理图集,如图6.20左侧所示。这里需要注意的是,图集中子纹理的形状和尺寸可以是任意的(如图6.6所示),Noll和Stricker提出了用于优化子纹理布局的方法。在生成和访问图集mipmap的时候也需要格外当心,因为mipmap的上一层级中可能会包含几个独立的、不相关的形状;Manson和Schaefer提出了一种通过考虑表面参数化来优化mipmap创建的方法,该方法可以生成更好的结果。Burley和Lacewell提出了一个叫做Ptex的系统,在该系统中,位于细分表面的每个四边形,都具有属于自己的迷你纹理,这样做的优点在于,避免了在网格上分配唯一的纹理坐标,并且在纹理图集不相连接部分的接缝处也不会出现瑕疵。为了能够实现跨四边形的过滤操作,Ptex系统使用一些邻接数据结构(adjacency data structure)。虽然Ptex系统最初的目标是用于渲染,但是Hillesland提出了packed Ptex,它将每个表面的子纹理都放入一个纹理图集中,在过滤的时候使用相邻表面作为填充从而避免间接取值。Yuksel提出了网格颜色纹理(mesh color texture),该方法对Ptex进行了改进。Toth实现了一种方法,即当 filter tap超出 [ 0 , 1 ] 2 [0,1]^2 [0,1]2的范围时,则将它们丢弃,从而为Ptex-like的系统提供了高质量的跨表面过滤。
当使用 wrapping/repeat或者mirror模式的时候,是无法使用纹理图集的,因为这些模式会对整个纹理产生影响,导致无法对子纹理进行正确的设置。另一个问题发生在为图集生成mipmap时,图集中的子纹理可能会与另一个子纹理相互混合。当然这个问题也有对应的解决方案,可以将子纹理的分辨率设置为2的整数次幂,然后在将每个子纹理放入纹理图集之前,提前为它们生成mipmap层次结构。
对于上述的这些问题,一个更简单的解决方案是使用一种被称为纹理数组(texture array)的API结构,它完全避免了mipmap和repeat模式所带来的问题,如图6.20右侧所示。一个纹理数组中的所有子纹理都需要具有相同的尺寸、格式、mipmap层次结构和MSAA设置。与纹理图集一样,纹理数组只需要进行一次设置,然后就可以通过着色器中的索引来访问数组中的任何元素,这种方法要比分别绑定每个子纹理快5倍。
现代图形API对于无绑定纹理(bindless texture)的支持,也有助于避免状态切换所带来的额外开销。如果没有无绑定纹理的话,则需要使用API来将纹理绑定到特定的纹理单元中,这会带来很多问题,其中一个问题是纹理单元数量的是有上限的,这使得程序员的工作变得更加复杂。在这种情况下,是由驱动程序来确保纹理驻留在GPU端的。对于无绑定纹理而言,纹理的使用数量是没有上限的,因为每个纹理都只通过一个64位的指针(有时称为句柄handle)来与其数据结构相关联。可以通过多种方式来访问这些句柄,例如通过uniform buffer、可变数据、其他纹理,以及着色器存储缓冲对象(shader storage buffer object,SSBO)等。应用程序需要确保纹理驻留在GPU端。无绑定纹理避免了驱动程序中任何类型的绑定开销,这使得渲染速度更快。
纹理压缩
固定压缩比的纹理压缩(fixed-rate texture compression)是一种直接解决内存、带宽以及缓存问题的解决方案。通过让GPU对纹理进行实时的解码压缩,能够使得纹理占据更少的内存,从而增加有效的缓存大小。同样重要的是,这样的纹理使用起来会更加高效,因为在访问纹理时,对于内存带宽的开销变少了。能够对纹理进行压缩,也意味着能够支持更大尺寸的纹理。例如:在 512 × 512 512\times512 512×512的分辨率下,每个纹素使用3个字节的未压缩纹理将会占用768 kB空间;而在使用纹理压缩之后(例如压缩比为 6 : 1 6:1 6:1),一个 1024 × 1024 1024\times 1024 1024×1024尺寸的纹理也只需要512 kB的空间。
有许多用于图像文件的压缩格式,例如JPEG和PNG格式,但是在硬件中实现这些图像解码的成本很高(有关纹理转换编码的内容)。S3开发了一种被称为S3纹理压缩(S3 Texture Compression,S3TC)的方案,它被作为DirectX的标准纹理压缩方法,被称为DXTC,在DirectX 10中则被称为BC(块压缩,Block Compression);此外,它也是OpenGL事实上的标准,因为几乎所有GPU都支持这种纹理压缩方法。这种方法的优点在于,可以创建大小固定的压缩图像,具有独立编码的片段,并且解码过程十分简单(因此速度很快)。图像的每个压缩部分都可以被单独解码,没有共享查找表或者其他依赖关系,这简化了解码过程。
DXTC/BC压缩方案有七种变体,它们之间有一些共同的特性。这种压缩方案的编码是在 4 × 4 4 × 4 4×4范围内的纹素块上(也称为tile)上完成的,每个纹素块都可以进行单独编码;这个编码过程是基于插值的,对于每个编码量,会存储两个参考值(即颜色)。它会在两个参考值之间所构成的直线上选择一个值,即在两个参考颜色之间进行插值。这种压缩方案最终只会存储两个参考颜色,以及每个像素的短索引值。
表6.1 不同的纹理压缩格式。表中的所有压缩方法都作用于 4 × 4 4 \times 4 4×4的纹素块上。表中的Storage列代表了最终存储所占据的空间,其中前面的是每个纹素块的所占据的字节数(byte,B),后面的是每个纹素所占据的bit数(bits per texel,bpt)。在Ref colors列中,前面的英文符号代表具体的通道情况,后面的数字代表了每个通道所占据的bit数。例如:RGB565代表了红色通道有5个bit,蓝色通道有6个bit,绿色通道有5个bit。
具体的编码方式在七个变体之间有所不同,它们之间的区别如表6.1所示。表中的“DXT”代表了DirectX 9中的名称,而“BC”则代表了DirectX 10及之后的名称。从表中我们可以看出,BC1有两个16 bit的RGB参考值(红色5 bit,绿色6 bit,蓝色5 bit),每个纹素都有一个2 bit的插值因子,用于在参考值或者两个中间值中进行选择;因为2 bit可以代表四个不同的值,其插值结果共有四个,分别是两端的参考值以及两个中间值。与未压缩的24 bit RGB纹理相比,这种压缩方法具有 6 : 1 6:1 6:1的纹理压缩比。
另一种DXT1模式为透明像素保留了四个可能的插值因子中的一个,此时由于alpha值的存在,可能的像素颜色数量会被限制为三个——两个参考值及其平均值。
BC2与BC1对于颜色的编码方式是相同的,但是为每个纹素添加了4个bit,用于存储alpha值。到了BC3中,每个纹素块对RGB数据的编码方式与DXT1相同;不同之处在于,BC3的alpha数据使用两个了8 bit的参考值,以及使用了每个纹素3 bit的插值因子进行编码,3 bit可以产生8个不同的值,一共会产生8个插值结果,分别是两端的参考值以及六个中间值,因此每个纹素可以在一个参考alpha值,或者六个中间值中进行选择。BC4只有一个通道,它与BC3中的alpha值使用了相同的编码方式。BC5包含了两个通道,每个通道都与BC3中的alpha值使用了相同的编码方式。
BC6H针对高动态范围(HDR)纹理进行压缩,在这个格式中,每个原始纹素的RGB通道都是一个16 bit的浮点数。BCH6会使用16个byte来存储一个纹素块,即每个纹素8 bit。BC6H具有两种模式,其中一种模式用于单条颜色线(类似于上面的压缩方法),另一种模式用于两条颜色线。在双颜色线模式下,每个纹素块都可以从一小组分区中进行选择。两个参考颜色也可以使用增量编码的方式,从而获得更好的精度表现,并且还可以根据使用的模式,应用不同的精度。在BC7中,每个纹素块可以有1到3条颜色线,每个纹素使用8 bit进行存储;BC7的目标是对8 bit RGB或者RGBA纹理进行高质量压缩。它与BC6H之间具有许多相同的属性,不同之处在于,BC7用于LDR纹理的压缩,而BC6H则用于HDR纹理的压缩。BC6H和 BC7在OpenGL中分别被称为BPTC_FLOAT和BPTC。这些压缩技术不仅可以应用于二维纹理,同样也可以应用于立方体贴图或者体积纹理的压缩。
这些压缩方案的主要缺点在于,它们都是有损压缩,也就是说,通常无法从压缩纹理中还原出原始图像。在BC1-BC5中,只使用了4或者8个插值出的值,来表示全部的16个像素。如果一个纹素块中包含大量不同的颜色值,那么就会不可避免的产生一些颜色信息的损失。在实际应用中,如果使用得当,这些压缩方案通常可以提供可接受的图像保真度。
BC1-BC5存在的一个问题是,由于仅仅使用了两个颜色参考值,因此在解码之后,一个纹素块中的所有颜色都会位于RGB空间中的一条直线上,例如:无法在一个纹素块中同时表示红色、绿色和蓝色。BC6H和BC7支持更多的颜色线,因此可以提供更高的质量。
OpenGL ES选择了另一种压缩算法,被称为Ericsson纹理压缩(Ericsson texture compression,ETC),这种压缩算法被内置在API中。该方案具有与S3TC相同的特点,即快速解码、随机访问、无间接查找和固定压缩比。它将 4 × 4 4×4 4×4的纹素块编码为64 bit,即每个像素使用4 bit,其基本思想如图6.21所示。每 2 × 4 2 × 4 2×4块(或者 4 × 2 4 × 2 4×2,取决于哪个的质量最好)会存储一个基色(base color)。每个纹素块还会从一个很小的静态查找表中选择四个常量,纹素块中的每个纹素,都可以选择机上其中的一个值,这会逐像素的修改其亮度值。这种压缩算法的图像质量与DXTC相当。
图6.21 ETC压缩方法会对像素块的颜色进行编码,然后通过逐像素的亮度修改来获得最终的纹理颜色。
在OpenGL ES 3.0中包含的ETC2中,使用了未使用的bit组合方式,来为原始ETC算法添加更多的模式。其中一个未使用的bit组合是压缩表示(例如:64 bit),它代表了这个纹素块会被解压为与另一个压缩表示完全相同的图像。例如:在BC1中,将两个参考颜色设置为相同颜色是没有意义的,因为这个纹素块中的所有纹素都将具有完全相同的颜色;而现在只要将一个参考颜色设置为该恒定颜色,就可以让整个纹素块都变成这个颜色。在ETC中,一种颜色也可以从第一个有符号颜色中进行增量编码获得,当然计算可能会造成数值溢出,这种情况被用来表示其他压缩模式。ETC2中添加了两个新模式,在第一种模式中,每个纹素块都有四种不同的颜色;第二个模式被称为最终模式,该模式是RGB空间中的一个平面,旨在处理参考颜色之间的平滑过渡。Ericsson alpha压缩(Ericsson alpha compression,EAC)用于对只包含一个通道的图像(例如alpha)进行压缩,这种压缩方法类似于基本的ETC压缩,二者的区别在于,前者只针对一个通道进行压缩,最终生成的图像会使用4 bit来存储一个纹素。EAC压缩方法还可以与ETC2相结合;此外,还可以使用两个EAC通道来对法线进行压缩(随后将详细介绍这个话题)。ETC1、ETC2与EAC都是OpenGL 4.0核心配置文件、OpenGL ES 3.0、Vulkan和Metal中的一部分。
对于法线贴图的压缩需要注意一些问题,因为针对RGB颜色进行设计的压缩格式,通常并不适用于法线的 x y z xyz xyz数据。大多数法线贴图的存储方法,都会利用已知法线为单位长度这个事实,并进一步假设其 z z z分量为正(对于切线空间法线而言,这是一个合理的假设)。这样就可以只存储法线的 x x x和 y y y分量,然后在运行时计算出 z z z分量即可:
n z = 1 − n x 2 − n y 2 (6.4) n_{z}=\sqrt{1-n_{x}^{2}-n_{y}^{2}} \tag{6.4} nz=1−nx2−ny2(6.4)
这种表示方法本身就带会来一些压缩效果,因为最终只需要存储两个分量即可。并且,由于大多数GPU并不原生支持三分量的纹理,一般只会原生支持四分量的纹理,但是这样的话就会浪费一个分量;而将三分量法线转换为两分量进行存储,就可以避免对第四个分量空间的浪费。对 x x x分量和 y y y分量的进一步压缩,通常是使用BC5或者3Dc格式来实现的,如图6.22所示。由于每个纹素块中的参考值限制了 x x x分量和 y y y分量的最大值和最小值,因此它们可以被视为在 x y xy xy平面上的轴对齐包围盒。3 bit的插值因子可以产生8个插值数据,即每个轴可以在8个值中任选其一,因此这个包围盒被划分为一个 8 × 8 8 × 8 8×8的潜在法线网格。或者也可以使用两个EAC通道来存储 x x x分量和 y y y分量,然后再按照方程6.4来计算 z z z分量。
图6.22 左侧:球面上的单位法线只需要编码x分量和y分量即可。右侧:对于BC4/3Dc,xy平面上的一个轴对齐包围盒限制了法线的范围,每个4 × 4的纹素块可以在这个8 × 8网格中选择法线(为了清晰,这里只显示4 × 4法线)。
在不支持BC5/3Dc或者EAC压缩格式的硬件上,一种常见的备用方法(fallback)是使用DXT5格式纹理,并将两个分量分别存储在绿色和alpha通道中(因为这两个通道的存储精度最高),剩下的红色通道和蓝色通道则没有使用到。
PVRTC也是一种纹理压缩格式,它可以在Imagination Technologies的硬件(叫做PowerVR)上使用,它最广泛的应用是在iphone和ipad上。它同样也是对 4 × 4 4 × 4 4×4的纹素块进行压缩,同时它对每个纹素都提供了两种存储方案,分别是2 bit或者4 bit。其核心思想是,提供两个图像的低频(平滑)信号,这两个信号是通过对相邻纹素块进行插值获得的,然后在解码图像的时候,每个纹素会使用1 bit或者2 bit的插值因子,来在这两个信号之间进行插值。
自适应可伸缩纹理压缩(Adaptive scalable texture compression,ASTC)的不同之处在于,它可以将 n × m n × m n×m的纹理块压缩成128 bit,其中纹素块的尺寸范围可以从 4 × 4 4 × 4 4×4到 12 × 12 12 × 12 12×12,纹素块尺寸的不同会导致不同的压缩比:当纹素块尺寸为 4 × 4 4 × 4 4×4时,每个纹素会使用8 bit进行存储;当纹素块尺寸为 12 × 12 12 × 12 12×12时,每个纹素仅会使用0.89 bit进行存储。ASTC使用了大量技巧来压缩索引表示,并且纹素块都可以选择不同的颜色线数量和端点(参考值)编码。此外,ASTC可以处理任意1-4通道的纹理,包括LDR纹理和HDR纹理。ASTC是OpenGL ES 3.2及后续版本中的一部分。
上面提到的所有纹理压缩方案都是有损的,而且不同压缩方案所花费的压缩时间也是不同的。花费较长的时间(几秒钟甚至几分钟)来进行压缩,可以获得更好的压缩质量;因此这个纹理压缩的过程通常是离线预处理的,即将压缩好的图像存储起来,并在之后进行使用。或者,只花费较短的时间(几毫秒)来进行压缩,这样获得的压缩质量通常较低,但是好处在于,在接近实时的情况下,对纹理进行压缩并立即使用。一个常见的例子是天空盒,当云的移动速度很慢时,天空盒每隔一秒左右才会刷新。纹理的解压缩过程是非常快的,因为它是使用固定功能的硬件来实现的;这种差异被称为数据压缩的不对称性(data compression asymmetry),即数据的压缩过程要比解压过程花费更长的时间。
图6.23 在纹理压缩过程中,每个通道使用16 bit与8 bit的效果对比。从左到右分别是:原始纹理;对每个通道8 bit的纹理使用DXT1进行压缩;对每个通道16 bit的纹理使用DXT1进行压缩,并在着色器中进行了重新归一化。为了更清楚地展示效果差异,使用了较强的光照来渲染纹理。
Kaplanyan 提出了几种可以提高压缩纹理质量的方法:对于包含颜色贴图和法线贴图的纹理,建议每个通道使用16 bit;对于颜色纹理而言,可以对这16个bit进行直方图归一化(histogram renormalization),然后在着色器中使用比例常量和偏移常量(每个纹理)来反转它的效果。直方图归一化是一种将图像中使用的颜色值扩展到整个范围(例如 [ 0 , 255 ] [0,255] [0,255])的技术,这是一种增强对比度的有效方法。每个分量使用16 bit,可以确保在重新归一化之后,直方图中不会出现未使用的位置,从而减少了许多纹理压缩方案可能会引入的带状瑕疵,如图6.23所示。此外,如果图像中有75%的像素值大于116/255的话,Kaplanyan建议对这种纹理使用线性颜色空间,而不是将纹理存储在sRGB颜色空间中。对于法线贴图而言,他还注意到BC5/3Dc压缩方案通常会独立于 y y y分量来压缩 x x x分量,这意味着并不总是能找到最佳的法线;相反,他建议对法线使用以下的误差度量:
e = arccos ( n ⋅ n c ∥ n ∥ ∥ n c ∥ ) (6.5) e=\arccos \left(\frac{\mathbf{n} \cdot \mathbf{n}_{c}}{\|\mathbf{n}\|\left\|\mathbf{n}_{c}\right\|}\right) \tag{6.5} e=arccos(∥n∥∥nc∥n⋅nc)(6.5)
其中 n \mathbf{n} n是原始法线, n c \mathbf{n}_c nc是经过压缩和解压缩的对应法线。
需要注意的是,也可以在不同的颜色空间中来进行纹理压缩,这可以对纹理压缩过程进行加速,一个常用的颜色空间变换是RGB → → →YCoCg,其数学形式如下:
( Y C o C g ) = ( 1 / 4 1 / 2 1 / 4 1 / 2 0 − 1 / 2 − 1 / 4 1 / 2 − 1 / 4 ) ( R G B ) (6.6) \left(\begin{array}{c}Y \\ C_{o} \\ C_{g}\end{array}\right)=\left(\begin{array}{rcr}1 / 4 & 1 / 2 & 1 / 4 \\ 1 / 2 & 0 & -1 / 2 \\ -1 / 4 & 1 / 2 & -1 / 4\end{array}\right)\left(\begin{array}{l}R \\ G \\ B\end{array}\right) \tag{6.6} YCoCg = 1/41/2−1/41/201/21/4−1/2−1/4 RGB (6.6)
其中的 Y Y Y是亮度项(luminance), C o C_{o} Co代表了橙色色度(chrominance), C g Cg Cg代表了绿色色度。这个变换过程的逆变换开销也很低:
G = ( Y + C g ) , t = ( Y − C g ) , R = t + C o , B = t − C o (6.7) G=\left(Y+C_{g}\right), \quad t=\left(Y-C_{g}\right), \quad R=t+C_{o}, \quad B=t-C_{o} \tag{6.7} G=(Y+Cg),t=(Y−Cg),R=t+Co,B=t−Co(6.7)
这两个变换都是线性的,方程6.6是一个矩阵和向量的乘法,这个运算本身就是线性的(详见方程4.1和方程4.2)。这一点十分重要,因为可以在纹理中存储YCoCg颜色,而不是RGB颜色;而且纹理硬件同样可以在YCoCg颜色空间中进行过滤操作,然后像素着色器可以根据需要,再将颜色转换回RGB颜色空间中。需要注意的是,这个转换过程本身是有损的,根据情况的不同,这对结果而言可能会很重要,也可能是无关紧要的。
还有一种可逆的RGB → → →YCoCg变换,其数学形式可以总结为:
{ C o = R − B t = B + ( C o ≫ 1 ) C g = G − t Y = t + ( C g ≫ 1 ) ⟺ { t = Y − ( C g ≫ 1 ) G = C g + t B = t − ( C o ≫ 1 ) R = B + C o (6.8) \left\{\begin{aligned} C_{o} & =R-B \\ t & =B+\left(C_{o} \gg 1\right) \\ C_{g} & =G-t \\ Y & =t+\left(C_{g} \gg 1\right)\end{aligned} \Longleftrightarrow \left\{ \begin{array}{rl} t & =Y-\left(C_{g} \gg 1\right) \\ G & =C_{g}+t \\ B&=t-\left(C_{o} \gg 1\right) \\ R & =B+C_{o} \end{array}\right. \right. \tag{6.8} ⎩ ⎨ ⎧CotCgY=R−B=B+(Co≫1)=G−t=t+(Cg≫1)⟺⎩ ⎨ ⎧tGBR=Y−(Cg≫1)=Cg+t=t−(Co≫1)=B+Co(6.8)
方程中的符号 ≫ \gg ≫代表了位运算中的右移操作,这意味着可以在24 bit的RGB颜色与相应的YCoCg表示之间来回转换,并且不会有任何损失。需要注意的是,如果RGB中的每个分量都有 n n n个bit,那么色度值 C o C_{o} Co和 C g C_g Cg便会各有 n + 1 n + 1 n+1个bit,从而保证变换是可逆的;而亮度值 Y Y Y只需要 n n n个bit。Van Waveren和Castano使用了有损的YCoCg变换,从而在CPU和GPU上实现了对DXT5/BC3格式纹理的快速压缩。它们将 Y Y Y存储在alpha通道中(因为它具有最高的精度),将 C o C_{o} Co和 C g C_g Cg存储在RGB的前两个通道中。这个压缩过程非常快,因为 Y Y Y是单独进行压缩和存储的;对于 C o C_{o} Co和 C g C_g Cg分量,他们构建一个二维包围盒,并选择能够产生最佳效果的对角线包围盒。请注意,对于在CPU上动态创建的纹理,最好也压缩CPU上对纹理进行压缩;而当纹理是通过GPU渲染创建的时候,通常最好也是在GPU上对纹理进行压缩。YCoCg变换和其他亮度-色度(luminance-chrominance)的变换,在图像压缩中十分常用,其中的色度分量会在 2 × 2 2 × 2 2×2的像素块上进行平均,这样做可以减少50%的存储空间,并且通常效果也很好,因为像素之间的色度变化是十分平缓的。Lee-Steere和Harmon将其进一步转换到HSV(hue-saturation-value)颜色空间中,并在 x x x和 y y y方向上对色调和饱和度进行4倍的下采样,最终将其存储为单通道的DXT1纹理。Van Waveren和 Castano还描述了法线贴图的快速压缩方法。
Griffin和Olano的一项研究表明,当多个纹理应用在一个具有复杂着色模型的几何物体上时,可以使用的质量很低的纹理,同时并不会带来任何可以感知的明显差异。因此,根据纹理使用情况的不同,可以对其质量进行一定程度的降低。Fauconneau提出了一种DirectX 11纹理压缩格式的SIMD实现。