Chapter 6 – Texturing 贴图

6.2 图像贴图

在图像贴图中,一张二维的图像将被“黏贴”到一个或者多个三角形的表面。我们已经学习了如何计算贴图空间坐标;现在我们将学习如何基于坐标来读取贴图中的贴图值。在之前的章节中,我们提到,一个像素实际上是一个显示的颜色值,它会被与其相关的屏幕网格单元外部的采样点所影响。

本小节中,我们将专注于快速采样并且filter贴图图像的方法。之前我们学习了锯齿的问题,尤其在绘制物体的边界时。贴图也会遇到采样问题,但是只会发生我们所绘制的三角形的内部。

像素着色器通过将贴图坐标值传入函数来读取贴图。贴图坐标(u,v)通过映射函数被映射至范围[0.0,1.0]。GPU将会把这个坐标转换为纹理坐标。在不同的API中,贴图坐标系统的区别主要有两个。在DX中,贴图的左上角的坐标为(0,0)而右下角为(1,1)。其与大多数图像格式存储数据的方式相同,也就是说顶部的纹理是文件中的第一行数据。在OpenGL中,纹理(0,0)位于左下角,其y轴与DX相反。纹理的坐标由整型数表示,但是通常我们会想要读取两个纹理之间的位置的数据。这就带来了问题,像素中心的浮点数坐标是什么。Heckbert提出有两种可行的系统:truncating和rounding。DX9将每个像素的中心点定义为(0.0,0.0),这就是rounding。但是,这个系统有些令人摸不着头脑,因为其表示贴图中左上角像素的左上角的点在像素内的坐标为(-0.5,-0.5)。在DX10与之后的版本中,DX使用了OpenGL的标准,也就是说,纹理中心的点的小数值为(0.5,0.5)——也就是truncating,或者更准确的说是flooring(上浮)。例如,一个像素的纹理坐标为(5,9),那么其定义的u轴范围是5.0到6.0,v轴范围是9.0到10.0。

还有一个需要解释的术语是dependent texture read,其有两个定义。首先其运用于移动设备,当我们在着色器中通过texture2D或者类似的数据格式读取贴图时,只要当像素着色器计算贴图坐标而不是使用顶点着色器所传入的贴图坐标时,dependent texture read就会发生。请注意,任何对于贴图坐标的修改,例如简单的调换u轴和v轴坐标,都会造成dependent texture read。从前的移动平台GPU,我们指的是那些不支持OpenGL ES 3.0的GPU,当着色器中没有dependent texture read时,运行的效率更高。因为这样的话,纹理数据可以预读取。另一个定义则是针对从前的桌面GPU。当一个贴图坐标取决于某些贴图的值时会造成dependent texture read。例如,一张贴图可能改变了着色发现,也就是说其改变了用于读取cube map的坐标。早期的GPU在这方面有着种种限制,甚至都不支持这一功能。现在,这一类的读取操作会影响性能,具体则取决于一个批次中处理的像素数量和一些其他因素。我们将在第二十三章再来学习这方面的知识。

GPU所使用的贴图图像尺寸一般为2m×2n,其中m和n都是非负整数。我们将这一类贴图称为power-of-two(POT,2次幂)贴图。现代的GPU能够处理non-power-of-two(NPOT,非2次幂)贴图。但是一些较早的GPU可能不支持NPOT贴图的mipmap。图形加速器对于贴图尺寸有着不同的上限标准。DX12最多支持163842个纹理。

假设我们有一张尺寸为256×256的贴图,想要用在一个正方形上。只要投影在屏幕上的正方形的尺寸与贴图尺寸相当,正方形上的贴图看起来与原始图像基本类似。但是如果投影的正方形覆盖了十倍于原始图像尺寸的像素(其被称为magnification),或者说如果投影的正方形只覆盖了一小部分屏幕(其被成为minification)?答案则取决于我们所使用的采样与filter。

我们在本章节中学习的图像采样与filter的方法只是运用于从贴图中读取的值。但是,对于最终的渲染图像的抗锯齿操作则需要对于最终像素颜色进行采样和filtering。这两者的区别是对着色等式的输入参数进行filtering还是对着色等式的输出结果进行filtering。只要输入与输出是线性关联的(例如着色等式中的颜色输入),那么对于贴图值进行filtering等同于对最终颜色进行filtering。但是,贴图中还会存储其他数据,例如表面的法线向量,粗糙度等等,其与输出的结果并不成线性关系。因此标准的贴图filtering可能并不能解决这一类贴图的问题,这就会导致锯齿和其他显示错误。对于这一类贴图的filtering将在第九章中学习。

6.2.1 Magnification

下图是一张尺寸为48✖48的贴图附着在一个正方形上,相对于贴图的尺寸,我们的观察距离非常近,所以这会造成图形系统放大这张贴图。magnification时最为常用的filtering技术是nearest neighbor(我们可以将其认为是box filter)和双线性插值(bilinear interpolation)。还有cubic convolution,其使用一个4✖4或者5✖5的纹理数组的权重总和。这能够大大改进magnification的质量。虽然现在硬件可能不支持cubic convolution,但我们可以通过着色器程序实现这一类型的filter。

上图左侧,我们使用了nearest neighbor的方法,也就是说直接选取最近的纹理值作为像素的颜色。而这一类的magnification的特点就是单个纹理变得十分明显。这一效果被称为pixelation(像素化)

上图中间则使用了双线性插值(有时我们也称其为线性插值)。对于每一个像素,该类型的filtering会找到四个相邻的纹理并且在两个方向上进行线性插值以计算出一个融合值对应该像素。图像看上去更模糊了,而且与左侧的图片相比,少了许多锯齿。

再回到我们之前举例的砖块贴图:如果我们不舍去小数部分,那么将得到(pu,pv)=(81.92,74.24)。我们使用OpenGL的左下角为原点的纹理坐标系统,因为其与笛卡尔坐标系相同。我们的目标是在四个最近的纹理间进行插值。如下图所示。为了找到最近的四个像素,我们将像素的坐标值减去(0.5,0.5),得到了(81.42,73.74)。除去小数部分,距离最近的四个像素的范围是(x,y)=(81,73)到(x+1,y+1)=(82,74)。小数部分,(0.42,0.74),则是我们的采样点基于四个纹理的中心的位置。我们将其称为(u’,v’)。

我们将贴图读取函数定义为t(x,y),其中x和y分别为整数且函数的返回值就是纹理。首先,底部的纹理,t(x,y)和t(x+1,y)进行横向插值(使用u’),同样也适用于顶部的两个纹理,t(x,y+1)和t(x+1,y+1)。也就是说,横向插值后的底部纹理为(1-u’)t(x,y)+u’t(x+1,y),而顶部的纹理为(1-u’)t(x,y+1)+u’t(x+1,y+1),在上图中我们使用绿色的圆点表示。之后,我们会对这两个值进行纵向插值(使用v’),所以最后双线性插值后的位于(pu,pv)颜色b为:

这也表明,如果一个纹理距离我们的采样坐标越近,它对颜色的影响也会越大。右上方位于(x+1,y+1)处的纹理对颜色值的影响为u’v’。请注意对称性:右上方的纹理的影响等同于采样点与左下角的纹理点所构成的矩形的面积。再回到我们的例子,这意味着读取右上方的纹理之后需要乘以0.42✖0.74,也就是0.3108。如果我们顺时针遍历其它三个采样点,那么它们的影响值分别为0.42✖0.26,0.58✖0.26和0.58✖0.74,所以四个采样点的权重总和为1。

使用细节贴图(detail texture)是一种常用的解决magnification所产生的模糊的方法。这些贴图通常带有表面的系列,例如划痕或者灰尘等等。这一类的细节贴图将作为一个独立贴图,以不同的缩放程度覆盖在被放大的贴图之上。高频率且不断重复的细节贴图,搭配地坪率的放大贴图,其产生的视觉效果类似于使用一张高分辨率的贴图。

双线性插值会在两个方向上进行插值运算。假设一张贴图由黑色和白色的像素组成,其排列类似于棋盘。使用双线性插值会改变贴图中的采样点的颜色。所以我们需要对采样点进行重新映射,例如,所有小于0.4的采样点都标记为黑色,所有大于0.6的都标记为白色,那些位于0.4和0.6之间的值将被拉伸,之后贴图即使被放大,看起来仍然是棋盘状的。如下图所示。

我们再回看本小节开始处的那组图片(经过magnification的女士),最右侧的图中,使用了bicubic的filter并且大大改善了锯齿。需要注意的是,bicubic的开销大于双线性插值。不过,许多效果更好的filter可以被转换为多个线性插值的组合(我们将在第十七章中学习)。因此,在贴图单元中GPU对于线性插值的支持可以被进一步扩展。

如果bicubic filter对于我们来说开销太大,Quilez提出了一种更为简单的技术,其使用了平滑曲线对一组2×2的纹理进行插值。我们将先为大家阐述这一曲线再来学习此技术。两种常用的曲线分别为平滑曲线(smoothstep curve)和五次方曲线(quintic curve):

当你想要将一个值平滑地插值为另一个值时,可以使用上述两种曲线。平滑曲线其特性为s'(0)=s'(1)=0,也就是说当x等于0或者1时,曲线的斜率都是0。五次方曲线也拥有相同的特性。两种曲线的示意图如下所示。

该技术也会计算(u’,v’)的颜色值(请参考之前的双线性插值),首先将贴图坐标乘以贴图的尺寸再加上0.5。我们将保留整数部分,并将小数部分存储为u’和v’,它们的范围都是[0,1]。之后(u’,v’)将被转换为(tu,tv)=(q(u’),q(v’)),其范围仍然是[0,1]。最后,我们减去之前的0.5并加上整数部分;u轴以及v轴坐标将分别被除以贴图的宽度与高度。此时,GPU将使用新的贴图坐标进行双向线性插值。需要注意的是,这一方法将时每一个纹理处于RGB空间中的平面(如上图所示,位于0或者1的点的斜率为0),这一类的插值比起线性插值更为平滑,但仍会产生“阶梯感”。如下图所示。

上图中,由左至右分别为nearest neighour(box),线性,五次方曲线以及cubic插值。

6.2.2 Minification

当贴图被缩小时,多个纹理将覆盖一个像素单元,如下图所示。

为了得到每个像素正确的颜色值,我们需要将所有会影响该像素的纹理进行合并。但是,我们很难精确地找出影响某一个像素点的多有纹理,同时这样做在实时渲染中也是不可能的。

正是由于这一限制,GPU采用了其他的方法。一种方法是使用距离最近的纹理,其原理与我们之前说的magnification的filter相同。这种filter会造成严重的锯齿问题。下图中,顶部的图片使用了这一类的filter。横向范围内的锯齿较为明显,这是因为许多个纹理影响一个像素,但是我们只选择了一个纹理来代表物体表面的像素点。

另一种常用的filter是双线性插值,其原理也与magnification下的双线性插值相同。但是这一种filter只是略微好于上一种filter。它将四个纹理进行融合以表示一个像素的颜色,但是当一个像素被超过四个纹理所影响,这种filter也会产生锯齿。

当然,我们可以选择更好的解决办法。正如我们在第五章中所了解的,锯齿的问题可以被分解为采样以及filter。一张贴图的信号频率则取决于它的纹理在屏幕中的紧密程度。由于Nyquist定理,我们需要保证,贴图的信号频率不会大于采样频率的一半。例如,一张图像又黑色和白色的线组成,其中隔着一个纹理。那么一个波长就是两个纹理这么宽(从黑线到黑线),所以频率为1/2。为了合理地将贴图展示在屏幕上,其频率至少为2×1/2,也就是说,至少一个像素对应一个纹理。所以,对于贴图来说,为了避免锯齿,一个纹理需要能够对应一个像素。

为了实现这一目标,我们需要增加像素的采样频率或者减少贴图的频率。在上一章中所讨论的抗锯齿方法就是增加了像素的采样率。但是,这只能略微增加采样频率。为了更好地解决这个问题,提出了不同的贴图缩小算法。

所有贴图抗锯齿算法的基本原理是一致的:预处理贴图并且创建数据结构帮助我们快速计算出一组纹理对一个像素的大致影响。对于实时渲染的应用,这些算法的特点是使用固定的时间与资源进行处理。因此,每个像素会采集一定数量的采样点并计算出纹理对于像素的影响。

上图中,顶部的图片使用了点采样(nearest neighbor),中间的图片采用了mipmapping,底部的图片则采用了summed area table。

Mipmapping

最常用的贴图抗锯齿方法被称为mipmapping。现在基本上所有的图形加速器都支持这一类的抗锯齿。“Mip”意味multurn in parvo,这是拉丁语,其意为“许多东西集中在一小块地方”——在图形学中,原始的贴图将被不断filter为尺寸更小的图像。

当我们使用使用mipmapping的filter时,在进行渲染之前,原始贴图将带有一组尺寸更小的贴图。贴图(等级为0)将以原始区域的四分之一进行降低采样,也就是说每一个新的纹理值为原始贴图中相邻四个纹理的平均值。而新生成的,等级为1的贴图被称为原始贴图的子贴图(subtexture)。这一降级操作不断重复直到贴图的一边或者两边只拥有一个纹理。其过程如下图所示。而这整个一组图像被成为mipmap chain

为了形成高质量的mipmap,我们需要选择好的filtering并且进行gamma correction。常见的形成mipmap的方法是取一组2×2的纹理并将它们进行平均以得到mip纹理值。而我们所使用的filter为box filter,也就是最为简单的filter。但是,这会导致子贴图的质量较差,而使用Gaussian,Lanczos,Kaiser或者类似的filter能改善子贴图的效果。

对于编码于非线性空间中的贴图(例如大多数的颜色贴图),如果忽略了gamma correction,那么filtering将会改变mipmap等级中贴图的亮度。当你距离物体较远时,没有进行gamma correction的mipmap将被使用,这会导致物体比平时看上去更暗,也会影响贴图的对比度与细节。因此,首先需要将贴图从sRGB转换至线性空间(请参考5.6小节),所有的mipmap filter需要在线性空间进行,并将最终的计算结果转换回sRGB颜色空间用于存储。大多数的API支持sRGB贴图,并且能够正确地生成位于线性空间的mipmaps再将结果存储回sRGB。当sRGB贴图被读取时,颜色值将先转换至线性空间,这样magnification和minification才能正常运行。

正如之前提到的,一些贴图与最终的着色值不成线性关系。这会影响filtering,同时对mipmap的生成也会造成影响,因为在mipmap生成之后需要对像素点进行filter。而我们将在第九章中学习具体的mipmap生成方法。

在贴图时读取mipmap结构的基本过程如下:首先,屏幕像素将会覆盖贴图上的一块区域。当像素的覆盖范围投影至贴图上时(如下图所示),其会包含一个或者多个纹理。使用像素单元的边界严格意义上来说是不正确的,但为了便于大家理解,所以在文中我们还是使用这一种方法。位于像素单元外部的纹理也会影响像素的颜色(例如,使用多重采样)。而我们的目标是大致计算有多少个纹理会影响该像素。常用的计算d(OpenGL称其为λ,其也被称为texture level of detail)的方法有两种。一种是使用像素单元所形成的四边形中较长的一边来估测像素的覆盖率;另一种是计算出四个导数中绝对值最大的那个倒数du/dx,dv/dx,du/dy,dv/dy。每一个倒数都表示贴图坐标基于屏幕轴向坐标的改变速率。例如du/dx表示沿着屏幕的x轴平移一个像素,贴图坐标u的改变量。

如果我们使用Shader Model 3.0或者更新的版本,那么在像素着色器中可以读取到上述梯度值(也就是导数值)。由于它们都是基于相邻像素之间的不同而得出的,我们无法在像素着色器中的动态分支(例如,if或者for循环)中读取这些值。因此,如果我们需要在动态分支中读取贴图,那么应该在进入分支之前计算导数值。需要注意的是,由于顶点着色器不能够读取梯度值,如果我们使用顶点贴图的技术,那么梯度值或者LOD的值需要我们自行在顶点着色器中进行计算。

我们计算d的值是为了决定使用哪一个mipmap。目标是为了达到像素与纹理的比例至少为1:1。其基本原理是,随着一个像素单元覆盖更多的纹理,且d不断增加,我们需要读取尺寸更小,更为模糊的贴图。三元坐标(u,v,d)将用于读取mipmap。d的值类似于贴图等级,但是其并非整型数,其小数部分表示两个等级之间的距离。也就是说,如果d=5.2,那么等级为5和等级为6的贴图都会被采样。我们将使用坐标(u,v)来读取两个等级的贴图中的双线性插值采样,之后这两个采样点会基于d的小数部分进行插值。这整个过程为成为三重线性插值(trilinear interpolation),而每一个像素都会进行这一操作。

而程序员可以通过level of detail bias(LOD偏移量)对坐标d进行控制。而这个值将会加上原始的坐标d,所以其会影响贴图的清晰度。如果偏移量越大,那么贴图看上去会更模糊。一个好的LOD偏移量需要基于图像的类型与其用处而改变。

而mipmapping的好处是我们不用再将所有影响一个像素点的纹理进行相加,只要读取预计算的一组纹理值并且进行插值即可。但是,mipmapping也有几个缺点。一个主要的问题是过于模糊。假设,一个像素单元在u轴方向覆盖了大量的纹理,但是在v轴方向只覆盖了一个纹理。这种情况通常发生在camera看着贴图表面的边缘。事实上,我们需要沿着贴图的某个坐标轴进行进行minification而沿着另一个坐标轴进行magnification。但是读取mipmap意味着我们在贴图中采样了一个正方形的区域而不是长方形的(例如,等级为1的mipmap尺寸为128×64,等级为2的mipmap尺寸则为64×32,四个组成正方形的纹理合并为了一个纹理)。为了避免锯齿,我们会选择像素单元在贴图上最大的覆盖率,也就是说选择较为模糊的mipmap。最后的结果就是读取的采样非常模糊。正如之前的三张网格图像中中间部分的效果。

Summed-Area Table

避免过度模糊的一种方法为summed-area table(SAT)。为了使用这一方法,需要先创建一个数组,其就是贴图的尺寸,但是每个颜色通道包含了更多的bit(例如,RGB分别为16-bit)。在数组中的每一个位置,成员必须计算并存储由该成员与贴图原点所构成区域中所有贴图纹理的总和。在贴图中,像素单元投影在贴图中的区域是一个长方形。之后读取SAT以决定该长方形区域的平均颜色值,并作为像素的贴图颜色。计算平均颜色值所需要的贴图坐标如下图所示。

其公式为:

其中,x和y分别表示矩形的纹理坐标,s[x,y]表示位于(x,y)处的纹理与原点纹理所覆盖区域的纹理总和。该公式的原理是求出包围盒(bounding box)的右上角所代表的纹理与原点纹理所覆盖的区域的纹理总和,之后在减去区域A和区域B。由于区域C被减了两次,所以需要在最后加上区域C,坐标(xll,yll)为区域C的右上角。

使用SAT的效果为之前的三张网格图片中底部的图片。横向的线段靠右侧的部分更为清晰,但是中部的交叉线段仍然较为模糊。这是因为当我们查看贴图时,像素单元投影至贴图空间上会是一个较大的矩形,而它形成的包围盒可能会包含大部分的贴图,也就是说会包含许多的纹理,这种情况下求出的平均值并不能代表像素单元所覆盖的纹理的平均值。

SAT只是anisotropic filtering算法中的一种。这一种算法可以从非正方形的区域内读取纹理值。但是,SAT只适合几乎水平或者垂直方向的矩形范围。还需要注意的是,如果使用的贴图尺寸为16×16,那么SAT所需要的贴图空间至少是其原始空间的两倍,如果我们使用精度更高的贴图,那么所需要的额外内存空间会更多。SAT虽然需要较高的内存空间但是能大大改善画面质量,且被大多数现代GPU支持。

Unconstrained Anisotropic Filtering

对于现在的图形硬件,常用的改善贴图filtering的方法基本是重复使用mipmap。其基本原理是将像素单元投影至贴图,形成一个四边形,之后进行多次采样并将采样结果进行融合。之前我们提到,每一个mipmap都拥有其坐标(d),且对应一个类正方形(例如,由四个纹理组成)。而我们的算法将使用多个mipmap所对应的正方形来代表像素单元四边形所覆盖的区域,而不是一个。像素单元所构成的四边形中叫短的一边将作为d(不同于之前的mipmapping,使用较长的一边);这意味着覆盖的区域较小(更清晰)。四边形较长的一边将用来创建一条平行线(anisotropy parallel)且穿过四边形的中点。当anisotropy介于1:1和2:1之间时,我们将沿着这条线取两个采样点(如下图所示)。anisotropy越高,那么沿着这条平行线的采样点会越多。

这个算法下,可以根据平行线的朝向进行采样,而不会像SAT的包围盒受制于像素单元投影后的四边形的方向。其基于mipmap,所以不会需要额外的存储空间。

6.2.3 体积贴图(Volume Textures)

三维的图像数据可以通过(u,v,w)进行读取。我们可以用这一理念来表示体积光/空间光。为了计算物体表面某一点的光照值,我们找到其在光源的空间中的位置,之后再结合光线的方向。

大多数GPU都支持空间体积贴图的mipmapping。由于在一个体积贴图的mipmap中就需要进行三重线性插值的filter,所以mipmap之间的filtering就需要四重线性插值(quadrilinear interpolation)。这意味着我们要对16个纹理进行平均,而数据的精度将成为一个问题,而使用一个更高精度的体积贴图能够解决这个问题。

虽然体积贴图将会大大增加贴图所占的存储空间,且filter操作的开销也会更大,但是体积贴图仍旧有着其独特的优势。由于我们可以直接使用三维坐标作为贴图坐标,我们不再需要计算出一个二位坐标来表示三维网格模型中某一点的纹理值。这避免了二位贴图坐标会造成的变形和接缝问题。体积贴图还能用于表示材质的体积结构,例如,木头或者大理石。如果一个模型使用了体积贴图,那么其质感会更好。

使用体积贴图作为物体表面的贴图是极其低效的,因为贴图中的大多数采样点都不会被使用。Benson,Davis以及Debry提出了将贴图数据存储在一个稀疏八叉树结构体(sparse octree structure)。这一算法适用于可交互的三维绘画系统,因为在创建表面时我们并不需要明确的贴图坐标。

6.2.4 Cube Map

另一种贴图是cube texture,也被称为cube map,其由六个正方形贴图组成,而每一张贴图表示立方体的每一个面。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)。cube map常用于环境贴图映射(我们将在第十章中学习)。

6.2.5 贴图的表现形式

当我们在应用中处理大量的贴图时,有几种方法能够提高性能。下一小节我们将学习贴图的压缩,而在本小节中我们将专注于贴图图集(atlases),贴图数组和非绑定贴图(bindless texture),这些操作都是为了在渲染时减少改变贴图时的开销。在第十九章中,我们将学习贴图流(streaming)和转码(transcode)。

为了让GPU能够处理更多的工作,一般我们会尽可能少地改变管线状态(pipeline state)。因此,多张贴图可能会合并为一张大的贴图,其被称为贴图图集(texture atlas)。如下方左侧的图片所示。

需要注意的是,子贴图的形状可以是任意的。此外,还需要注意mipmap的生成与读取,因为高等级的mipmap(尺寸较小的mipmap)可能包含一些独立的,不相关的形状。Manson和Schaefer提出了通过计算物体表面参数来优化mipmap创建的方法。Burley和Lacewell提出了一种系统,起被称为Ptex,物体表面的每一个四边形区域都有一个对应的小贴图。这么做的好处是避免了为整个网格模型的所有顶点设置贴图坐标,这样的话贴图图集中不相连部分的接缝处也不会有显示错误。为了能在四边形上进行filter,Ptex使用一种相邻的数据结构。

使用贴图图集时,如果address mode为wrapping或者mirror,那么会碰到一些问题。因为,当贴图坐标超过1时,受影响的不单单是子贴图而是整个贴图图集。而且在我们生成mipmap时,相邻的子贴图可能会互相影响。不过,后面这个问题可以避免,我们只需要先为子贴图单独生成自身的mipmap,之后再将其组成贴图图集即可。

为了解决wrapping和mirror的问题,我们可以使用贴图数组(texture array),如上图右侧部分所示。贴图数组中所有的子贴图需要拥有相同的尺寸,格式,mipmap层级以及MSAA设置(最新的shader model 5.0则支持不同尺寸的,不同格式的贴图打包为一个贴图数组)。与贴图图集相同,贴图数组只需要设置一次,之后我们就可以在着色器中使用索引来读取数组中的成员。与分别将子贴图进行绑定相比,使用贴图数组能将效率提高5倍。

另一种避免不断改变贴图所带来的开销的方法是API对于非绑定贴图的支持。如果没有bindless贴图,那么我们将使用API将贴图绑定至特定的贴图单元。而贴图单元的数量是有限制的,这意味着程序员需要确保贴图单元不会越界。驱动将确保贴图是寄存在GPU端的。有了非绑定贴图,那么对于贴图的数量就没有了限制,而每一个贴图将与一个64位且指向数据结构的指针相关联,有时我们也将其称为handle,我们可以用不同的方式去读取这些handle,而应用只需要保证贴图寄存在GPU端即可。非绑定贴图避免了驱动端在绑定贴图时产生的开销,这也会加速渲染。

6.2.6 贴图压缩

另一个解决贴图所带来的内存,带宽和缓存问题的方法就是贴图压缩(texture compression)。GPU将压缩后的贴图进行解码,那么原始贴图所需要的贴图内存就会降低。我们在读取贴图所需要的内存带宽也会减少,那么使用压缩贴图的效率也会更高。使用贴图压缩之后,我们就能够使用尺寸更大的贴图。例如,如果一张未压缩的贴图,其中的每一个纹理会占有3个字节,那么尺寸为512×512的贴图就会占有768kb。使用贴图压缩之后,如果压缩比率为6:1,一张1024×1024的贴图只会占有512kb。

不同图像文件格式,例如JPEG或者PNG,有着不同的图像压缩方法,但是硬件对于这一类压缩方式的解码都会产生较大的开销(我们将在第十九章学习贴图的转码)。S3开发了一种被称为S3 Texture Compression(S3TC)的技术,其已经被DirectX选为贴图压缩的标准——在DX10中,其被称为BC(Block Compression)。此外,由于几乎所有的GPU都支持这一技术,OpenGL也将其作为贴图压缩的标准。将图片压缩为固定大小/尺寸的独立编码块能够便于GPU进行解码,或者说加快解码的速度。图像的每一个压缩部分都能独立处理。也就是说,压缩块之间并没有共享的列表或者其他引用,这将大大简化解码过程。

DXTC/BC压缩技术拥有七种选择。编码将基于4×4的纹理块,也被称为tiles。每一个纹理酷块都是独立编码,且编码基于插值。对于每一个编码数量,两个引用值将被存储(例如,颜色值)。纹理块中的每一个纹理将会存储一个插值因子。我们将使用插值因子在两个引用值之间计算出纹理的原始颜色值,例如,两个引用值为(0,0,0)和(1,1,1)而某一个纹理的插值因子为0.5,那么其原始颜色就是(0.5,0.5,0.5)。

七种选择之间有着不同的编码参数,如下表所示。需要注意的是“DXT”是DX9所使用的名称而“BC”则是DX10与之后的DX版本所使用的名称。在列表中,BC1拥有两个16位RGB引用值(RGB565,Red 5位,Green 6位,Blue 5位),并且每一个纹理拥有一个2位的插值因子。相较于未压缩的24位RGB贴图,这意味着6:1的贴图压缩率。BC2与BC1使用相同的方式对颜色值进行编码,但是BC2为每一个纹理添加了4位的数据用于alpha值。BC3也有着相同的颜色值编码方式。此外,其对于alpha值的编码方式则变为使用两个8位的引用值以及每一个纹理携带一个3位的alpha插值因子。BC4的颜色值只有一个通道,其编码方式类似于BC3中的alpha值。BC5只包含两个通道,每一个通道的编码方式则类似于BC3。

上表展示了所有7种压缩选择,它们的压缩块都由4×4个纹理组成。“存储”那一列表示每一个压缩块所占字节(B)以及每一个纹理所占的位数(bpt,bit per texel)。“引用颜色值”则表示每一个通道所占位数。例如,RGB565意味着红色和蓝色通道占5而绿色通道占6。

BC6H则表示高动态范围(HDR)贴图,未压缩状态下RGB每一个通道都由16位浮点数表示。这一模式下,整个压缩块占16个字节,每一个纹理占8位。其中的一种模式使用单一线段插值(类似于BC1),而另一种模式使用两个线段。为了得到更高的精确度,两个引用颜色值可以进行delta-encoded,选择不同的模式可以得到不同的准确度。在BC7中,每一个压缩快可以拥有单一线段或者三个线段,每个纹理存储了8位数据。其目标是高质量的8位RGB和RGBA压缩贴图,原理与BC6H类似,但是适用于LDR贴图。需要注意的是,在OpenGL中这两种压缩格式被称为BPTC_FLOAT和BPTC。这些压缩技术也可以运用于cube贴图和体积贴图。

这些压缩技术的主要缺点是“损耗”。也就是说,我们很可能无法将压缩版本解码为原始贴图。BC1-BC5只使用4个或者8个插值来表示16个像素(如果插值因子占2位,那么插值因子的选择就是四种。如果插值因子占3位,那么选择就是8)。如果4×4的纹理互相之间差距较大,那么就会产生损耗。实践中,如果我们正确使用这些压缩模式,那么图像的正确性还是能够得到保障的。

BC1-BC5的主要问题是压缩块中所有的颜色为处在RGB空间中的直线上。例如,RGB的三个通道都位于同一个压缩块中。BC6H和BC7支持更多的线段,所以也能提供更高的质量。

对于OpenGL ES,另一个被成为Ericsson贴图压缩(ETC)的技术被纳入到其API中。这一技术将4×4的纹理编码至64位空间中,例如,每一个纹理占有4位。其基本原理如下图所示。每一个2×4或者4×2的压缩块存储一个基本颜色。每一个压缩块会从一个静态的查询列表中选择4个常量,而每一个压缩块中的纹理能够选择四个常量中的一个加上基本颜色。这将改变每个像素的“亮度”。图像质量与DXTC相当。

在OpenGL ES 3.0中的ETC2,未被使用的位将用来增加原始ETC算法的模式。例如,在BC1中,如果将两个引用颜色设成相同的值是没有意义的,这会导致压缩块中16个纹理的颜色都是相同的。在ETC中,一个颜色可以从第一个带有符号的颜色增量编码(delta encoding)得到。其可以用于标记ETC中的其他压缩模式。Ericsson alpha compression(EAC)压缩只包含一个通道(例如,alpha)的图像。这种压缩类似于基本的ETC压缩,而压缩后的图像中每个纹理只占有4位。其可以与ETC2相结合,此外还可以使用两个EAC通道来压缩法线贴图。ETC1,ETC2和EAC都属于OpenGL 4.0,OpenGL ES,Vulkan和Metal的一部分。

法线映射贴图的压缩则需要额外处理(我们将在之后的小节学习)。通常,RGB颜色值的压缩格式不适用法线的xyz格式。大多数压缩法线贴图的压缩原理基于法线贴图中法线的长度是单位长度,且默认认为z轴的坐标值是正数(对于切线空间的法线,这是一个合理的假设)。这意味着我们只需要存储法线向量的x轴坐标和y轴坐标即可。z轴的坐标通过以下等式得到:

这自然就形成了压缩,因为我们只需要存储两个成员而不是三个。由于大多数GPU并不是天生支持三个成员的贴图,这种压缩形式避免了浪费一个成员。此外,法线贴图的压缩通常会将x轴与y轴坐标存储在BC5格式的贴图中。如下图所示。由于每一个压缩块的引用值划分了x轴与y轴坐标的最大值和最小值,我们可以认为引用值定义了xy平面内的包围盒。插值因子是一个3位的数,因此每一个轴向上都有8个选择(2的三次方),所以这个包围盒能过够被划分为8×8的法线网格。或者,我们可以使用只包含两个通道的EAC压缩格式(x轴和y轴),而求出z轴的方法保持不变。

上图中左侧表示,单位法线向量只需要编码为x轴和y轴坐标即可。右侧则表示,对于格式BC4来说,xy平面内的包围盒封装了所有的法线,且每一个4×4的法线纹理压缩块能够表示的法线有8×8种选择(为了显示得更清晰,上图中我们画了4×4个法线)。

如果硬件不支持BC5/3Dc或者EAC的压缩格式,一个常用的备选方案是使用DXT5的压缩格式,并且将两个成员存储在G和alpha通道(由于这两个通道的精度最高)。

PVRTC(Power VR Texture Compression)是Imagination Technologies硬件平台上的贴图压缩格式,该格式在iPhone和iPad上被广泛使用。其压缩策略为每一个纹理占据2位或者4位,每个压缩块由4×4个纹理组成。其基本原理是为图像提供两个低频率的信号。之后每一个纹理使用1位或者2位的数据在两个信号之间进行插值。

ASTC(Adaptive scalable texture compression)将一组由n×m个纹理组成的压缩块压缩为128位。压缩块的尺寸为4×4到12×12,每一种占有不同的bit,最低为0.89位每个纹理,最高为8位每个纹理。ASTC支持基于压缩块选择插值线段和编码的起始点。此外,ASTC能够处理单个通道至四通道的贴图,也支持LDR和HDR贴图。其包含在OpenGL ES 3.2及以上版本中。

上述提到的所有贴图压缩技术都会有一定的细节丢失。在压缩一张贴图时,其话费的时间也取决与压缩的过程。当然,花费数秒,甚至几分钟来压缩贴图能够得到较高的压缩质量;因此,贴图压缩一般是一个预处理过程。但是,我们也可以只花费几毫秒以较低的质量压缩贴图。这样我们就能够在实时渲染进行压缩并使用贴图了。天空盒(skybox)就是这一类例子,其每隔数秒就会重新生成,并形成云朵缓慢移动的效果(我们将在第十三章中学习)。由于贴图的解压缩使用的是硬件所提供的固定函数,其速度非常快。而贴图压缩所需要的时间远远多于解压缩的时间,这一不同被称为数据压缩不对称(data compression asymetry)

Kaplanyan提出了多种改善压缩贴图质量的方法。对于颜色贴图与法线映射贴图,其建议每一个成员占有16位。对于颜色贴图,我们进行histogram renormalization,之后在着色器中使用一个范围与偏移常量。Histogram normalization能够将图像中的值进行扩展以扩大整个颜色值的范围,其有效地增加了对比度。每一个成员占有16位保证了在renormalization之后,在histogram中不会有未被使用的slot,其减少大多数贴图压缩技术会碰到的带宽问题。此外,Kaplanyan建议使用线性颜色空间,如果贴图中75%的像素都大于116/255,否则的话将贴图存储在sRGB空间。对于法线贴图,BC5/3Dc通常独立对x轴坐标进行压缩,这意味着我们可能找不到最佳的法线。相反,对于法线贴图,他提出使用以下误差测量:

其中,n表示原始的法线向量,nc表示压缩后被解压缩的法线向量。

此外,我们还可以将贴图压缩至不同的颜色空间,这能够加速贴图的压缩。常用的转换是RGB→YCoCg:

其中Y是亮度术语,Co和Cg是色度术语。其逆转换的等式如下,开销也非常小:

无论是矩阵向量的乘法运算还是其逆转换都是线性的。这也意味着,我们可以将贴图存储在YCoCg空间而不是RGB空间,之后像素着色器能够将YCoCg转换为RGB。需要注意的是,该转换也会丢失一定的细节。

另一种可逆的RGB→YCoCg转换,如下所示:

其中>>表示向右移位。这意味着RGB颜色和相对应的YCoCg之间的转换可以不丢失任何细节。需要注意的是,如果RGB中的每一个成员都占有n位,那么Co和Cg将占有n+1位,以保证转换是可逆的;而Y只需要n位。Van Waveren和Castano使用会丢失精度的YCoCg转换以实现快速压缩至DXT5/BC3。他们将Y存储在alpha通道中(由于其精确度是最高的),Co和Cg将存储在RGB的前两个通道中。由于Y是独立存储并压缩的,所以压缩的速度将变得更快。对于Co和Cg成员,他们发现一个二维的包围盒并选择包围盒的对角线能够提供最好的结果。需要注意的是,如果贴图在CPU端动态生成,那么我们最好在CPU端进行压缩。当贴图通过渲染在GPU端生成,最好也是在GPU端进行压缩。YCoCg转换和其他的亮度色度转换常用于图像的压缩,其中色度成员将在2×2的像素中进行平均。这能够降低50%的存储空间,由于色度的变换并不会那么剧烈,所以其效果也会较好。

Griffin和Olano提出,当一个模型使用多张贴图并运用了较为复杂的着色模式时,即使贴图的质量较低也不会造成明显的不同。

留下评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据