Texturing
本章详细介绍纹理技术。首先,给出了纹理处理的一般框架。接下来将重点放在使用图像来映射表面,因为这是实时工作中使用的最流行的纹理形式。之后对程序纹理进行了简要的讨论并介绍了几种常用的纹理处理方法。
表面的纹理就是它的外观和触感,想想油画的纹理就知道了。在计算机图形学中,纹理处理是一种使用图像、函数或其他数据源获取一个表面并在每个位置修改其外观的过程。例如,不是精确地表示砖墙的几何形状,而是将砖墙的彩色图像应用于由两个三角形组成的矩形。当矩形被查看时,彩色图像出现在矩形的位置。除非观者靠近墙壁,否则缺乏几何细节将不会被注意到。
然而,一些有纹理的砖墙可能是不令人信服的,不过不是因为缺乏几何形状。例如,如果灰泥是哑光的,而砖是光滑的,那么观察者会注意到两种材料的粗糙度是相同的。为了产生更令人信服的体验,可以在表面应用第二个图像纹理。这种纹理不会改变表面的颜色,而是根据表面的位置改变墙壁的粗糙度。现在砖块和灰泥有了来自图像纹理的颜色和来自这个新纹理的粗糙度值。
观察者可以看到现在所有的砖都是光滑的,而灰泥不是,但请注意,每个砖面似乎是完美的。这看起来不太对,因为砖块的表面通常有些不规则。通过应用凹凸贴图(bump mapping),砖块的法线可能会发生变化,这样当它们被渲染时,就不会显得非常光滑。为了计算光照,这种纹理会使矩形的原始表面法线方向发生抖动。
从一个浅的视角来看,这种凹凸的错觉可能会被打破。砖块应该突出在灰泥之上,挡住视线。即使从直观的角度看,砖块也应该在灰泥上投下阴影。视差贴图(Parallax mapping)使用纹理在渲染时使表面变形,而视差遮挡贴图(parallax occlusion mapping)将光线投射到高光场纹理上以提高真实感。位移贴图(Displacement mapping)通过修改形成模型的三角形高度,真正取代了曲面。
原书图6.1,纹理。颜色和凹凸贴图被应用到这条鱼,以增加它的视觉细节水平。
1纹理管线
纹理是一种有效地建模表面材质和表面粗糙度变化的技术。考虑纹理的一种方法是考虑单个被着色像素的情况。正如在前一章中所看到的,着色是通过考虑材料的颜色和灯光等因素计算出来的。如果存在透明度也会影响样本。纹理是通过修改着色方程中使用的值来实现的。这些值的变化方式通常基于表面的位置。因此,对于砖墙的例子来说,根据表面位置,将表面上任意一点的颜色替换为砖墙图像中相应的颜色。图像纹理中的像素通常称为texels,以便将它们与屏幕上的像素(pixels)区分开来。粗糙度纹理修改了粗糙度值,凹凸纹理改变了法线的方向,所以这些都改变了着色方程的结果。
纹理可以用广义纹理管线来描述。稍后将介绍许多术语并将详细描述管线的每个部分。
空间中的位置是纹理处理的起点。这个位置可以在世界空间中,但更多的时候是在模型的参考框架中,所以当模型移动时,纹理也随之移动。使用Kershaw的术语,空间中的这个点有一个投影(projector)功能,用于获取一组数字,称为纹理坐标(texture mapping),用于访问纹理。这个过程称为映射(mapping),因此有了术语纹理映射(texture mapping)。有时纹理图像(texture image)本身被称为纹理映射(texture map),尽管这并不完全正确。
在使用这些新值访问纹理之前,可以使用一个或多个对应函数将纹理坐标转换为纹理空间。这些纹理空间位置用于从纹理中获取值,例如,它们可以是数组索引到图像纹理中以检索像素。然后,通过值转换函数可能再次转换检索到的值,最后这些新值用于修改表面的某些属性,如材质或着色法线。图6.2详细显示了单个纹理应用程序的这个过程。管线复杂的原因是,每个步骤都为用户提供了一个有用的控件。不过并非所有步骤都需要在任何时候都被激活。
原书图6.2,用于单个纹理的通用纹理管线。
使用这个管线,当一个三角形具有砖墙纹理并在其表面生成一个示例时,就会发生这种情况(参见图6.3)。
原书图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节)。
1.1投影处理
纹理处理的第一步是获取曲面的位置,并将其投影到纹理坐标空间中,通常是二维(u,v)空间。建模通常允许艺术家定义每个顶点的(u,v)坐标。这些可以从投影函数或网格展开算法初始化。艺术家可以用编辑顶点位置同样的方式编辑(u,v)坐标。投影功能通常是通过将空间中的三维点转换为纹理坐标来实现的。建模程序中常用的函数包括球面、圆柱和平面投影。
其他输入可用于投影功能。例如,表面法线可以用来选择六个平面投影方向中的哪一个用于表面。纹理匹配问题发生在面相接的接缝处。Geiss讨论了一种混合它们的技术。Tarini等人描述了多立方体映射(polycube maps),其中模型映射到一组立方体投影,不同体积的空间映射到不同的立方体。
其他投影功能根本不是投影,而是表面创建和细分的一个隐式部分。例如,参数曲面有一组(u,v)作为其定义的一部分值。参见图6.4。纹理坐标还可以由各种不同的参数生成,比如视图方向、表面温度或任何可以想象的东西。投影功能的目标是生成纹理坐标。把这些作为位置的函数只是一种方法。
原书图6.4,不同纹理投影。球形,圆柱形,平面和自然(u,v)从左到右显示。下面一行显示了应用于单个对象(没有自然投影)的每个投影。
非交互式渲染器通常将这些投影功能称为渲染过程本身的一部分。一个简单的投影功能可以满足整个模型,但艺术家往往不得不使用工具来细分模型,并分别应用不同的投影功能。参见图6.5。
原书图6.5,如何在一个模型上使用不同的纹理投影。盒映射由六个平面映射组成,每个盒面对应一个平面映射。
在实时工作中,通常在建模阶段使用投影函数,将投影结果存储在顶点上。情况并非总是如此;有时在顶点或像素着色器中应用投影函数是有利的。这样做可以提高精度,并有助于实现各种效果,包括动画(第6.4节)。有些渲染方法,如环境映射(10.4节),具有自己的特殊投影功能,这些功能按像素计算。
球面投影(图6.4中的左边)将点投射到以某个点为中心的假想球体上。这个投影与Blinn和Newell的环境映射方案中使用的是相同的(第10.4.1节),之后的方程10.30描述了这个函数。这种投影方法也存在与该节中描述的顶点插值相同的问题。
圆柱投影计算的u纹理坐标与球面投影相同,v纹理坐标计算的是沿圆柱轴线的距离。这种投影对于具有自然轴的物体很有用,比如旋转面。当表面接近垂直于圆柱体轴线时,就会发生畸变。
平面投影就像一束x射线,沿着一个方向平行投影,并将纹理应用于所有表面。它使用正投影(第4.7.1节)。这种类型的投影适用于贴花(第20.2节)。
由于与投影方向平行的表面存在严重的失真,艺术家通常必须手工将模型分解成接近平面的部分。还有一些工具可以通过展开网格或创建一组近乎最优的平面投影来帮助最小化失真,或者帮助这个过程。我们的目标是让每个多边形在纹理的区域中得到更公平的份额,同时尽可能保持网格的连通性。连接性很重要,因为采样伪影可以出现在纹理的不同部分相接的边缘。一个良好展开的网格,也使艺术家的容易工作。第16.2.1节讨论了纹理失真对渲染的负面影响。图6.6显示了用于创建图6.5中雕像的工作区。这个展开过程是一个更大的研究领域,网格参数化(mesh parameterization)的一个方面。感兴趣的读者可以参考Hormann等人的SIGGRAPH课程笔记。
原书图6.6,雕像模型的几个较小的纹理,保存在两个较大的纹理中。右边的图显示了如何展开三角形网格并显示在纹理上,以帮助创建三角形网格。
纹理坐标空间并不总是二维平面;有时它是一个三维体。在这种情况下,纹理坐标表示为一个三元素向量(u,v,w),其中w为沿投影方向的深度。其他系统最多使用四个坐标,通常指定为(s,t,r,q);q是齐次坐标中的第四个值。它就像电影或幻灯片放映机一样,投影纹理的大小随着距离的增加而增加。例如,它可以将一种叫做gobo的装饰性聚光灯图案投射到舞台或其他表面。
纹理坐标空间的另一种重要类型是方向,其中空间中的每个点都由输入方向访问。将这样的空间可视化的一种方法是在一个单位球上的点,每个点上的法线表示用于访问该位置纹理的方向。使用方向参数化的最常见纹理类型是cube map(第6.2.4节)。
同样值得注意的是,一维纹理图像和函数也有它们的用途。例如,在地形模型上,颜色可以由高度决定,例如,低地是绿色的;山峰是白色的。线也可以被纹理;它的一个用途是将雨作为一组半透明图像纹理的长线条。这样的纹理对于从一个值到另一个值的转换也很有用,比如作为查找表。
由于一个表面可以应用多个纹理,因此可能需要定义多个纹理坐标集。无论如何应用坐标值,其思想都是一样的:这些纹理坐标在整个表面内插并用于检索纹理值。然而,在插值之前,这些纹理坐标被相应的函数转换。
1.2映射处理
映射函数(corresponder functions)将纹理坐标转换为纹理空间位置。它们提供了在表面上应用纹理的可行性。映射函数的一个例子是使用API选择要显示的现有纹理的一部分;只有这个子图像将在后续操作中使用。
另一种类型的映射是矩阵变换,它可以应用于顶点着色器或像素着色器。这会平移,旋转,缩放,剪切,或投射纹理的表面。正如第4.1.5节所讨论的,转换的顺序很重要。令人惊讶的是,纹理的转换顺序必须与我们所期望的顺序相反。这是因为纹理变换实际上影响了决定图像位置的空间。图像本身不是被转换的对象;定义图像位置的空间被更改。
另一类映射函数控制应用图像的方式。我们知道(u,v)在[0,1]范围内。但是在这个范围之外会发生什么呢?对应函数决定行为。在OpenGL中,这种类型映射函数称为”包装模式(wrapping mode)”;在DirectX中,它被称为”纹理寻址模式(texture addressing mode)。”这种类型的通用映射函数是:
-
wrap(DirectX),repeat(OpenGL),或tile——图像在表面重复自己;算法上,删除纹理坐标的整数部分。这个函数对于让一个物体的图像重复地覆盖一个表面很有用,并且通常是默认值。
-
mirror——图像在表面上重复,但在每一次重复时都会镜像(flipped)。例如,图像通常从0到1,然后在1和2之间反转,然后在2和3之间正常,然后反转,以此类推。这提供了沿着纹理边缘的连续性。
-
clamp(DirectX)或clamp to edge(OpenGL)——值超出范围[0,1]部分被限制在这个范围内。这导致图像纹理边缘的重复。当纹理边缘附近发生双线性插值时,此功能可用于避免从纹理的相对边缘意外地采样。
-
border (DirectX)或clamp to border (OpenGL)——超出[0;1]的纹理坐标使用单独指定的边框颜色渲染。例如,这个函数可以很好地在单色表面上呈现贴花,因为纹理的边缘将与边框颜色平滑地混合。
参见图6.7。这些对应的函数可以为每个纹理轴分配不同的值,例如纹理可以沿着u轴重复,也可以限制在v轴上。在DirectX中,还有一个mirror one模式,它沿着纹理坐标的零值对纹理进行一次镜像,然后进行箝位,这对于对称贴花非常有用。
原书图6.7,图像纹理重复repeat,镜像mirror,夹紧clamp和边框border功能。
重复平铺纹理是一种为场景添加更多视觉细节的廉价方法。然而,这种方法在重复三次纹理之后,通常看起来并不令人信服,因为眼睛会识别出图案。避免这种周期性问题的常见解决方案是将纹理值与另一个非平铺纹理相结合。这种方法可以得到很大的扩展,如Andersson所描述的商业地形绘制系统所示。该系统中,根据地形类型、高度、坡度等因素,将多种纹理进行组合。纹理图像还与几何模型(如灌木丛和岩石)在场景中的位置相关联。
避免周期性的另一个选择是使用着色器程序来实现特殊的映射函数,这些函数随机地重新组合纹理样式或平铺。Wang tiles就是这种方法的一个例子。Wang tile 是一组边缘匹配的方砖。在纹理处理过程中,tiles是随机选择的。Lefebvre和Neyret使用依赖的纹理读取和表实现了类似类型的映射函数,以避免模式重复。
最后一个映射函数是隐式的,由图像的大小导出。纹理通常应用在[0,1]对于u和v。如砖墙的例子所示,通过将这个范围内的纹理坐标乘以图像的分辨率,可以得到像素的位置。指定(u,v)取值范围[0,1]的优点是可以交换不同分辨率的图像纹理,而不需要改变模型顶点上存储的值。
1.3纹理值
在使用相应的函数生成纹理空间坐标后,使用坐标获取纹理值。对于图像纹理,这是通过访问纹理来从图像中检索图素信息来实现的。第6.2节详细讨论了这一过程。图像纹理在实时工作中占绝大多数,但也可以使用程序函数。在程序纹理的情况下,从纹理空间位置获取纹理值的过程不涉及内存查找,而是一个函数的计算。程序纹理将在6.3节中进一步描述。
最直接的纹理值是用于替换或修改表面颜色的RGB三元组;类似地,可以返回单个灰度值。返回的另一种数据类型是RGB$/alpha$,如第5.5节所述。alpha值通常是颜色的不透明度,它决定了颜色对像素的影响程度。也就是说,任何其他值都可以存储,比如表面粗糙度。还有许多其他类型的数据可以存储在图像纹理中,我们将在详细讨论凹凸贴图时看到(第6.7节)。
从纹理返回的值在使用之前可以选择进行转换。这些转换可以在着色器程序中执行。一个常见的例子是将数据从无符号范围(0.0-1.0)映射到有符号范围(-1.0-1.0),用于对存储在颜色纹理中的法线进行着色。
2图像纹理
在图像纹理处理中,二维图像被有效地贴在一个或多个三角形的表面上。我们已经完成了计算纹理位置的过程;现在我们将讨论从给定位置的图像纹理中获取纹理值的问题和算法。在本章的其余部分,图像纹理将被简单地称为纹理。此外,当我们在这里提到像素的单元格时,我们指的是围绕该像素的屏幕网格单元格。正如第5.4.1节所讨论的,像素实际上是一个显示的颜色值,它可以(而且应该)被与其相关的网格单元格之外的示例所影响。
在本节中,我们特别关注快速采样和处理纹理图像的方法。第5.4.2节讨论了锯齿问题,特别是对象的边缘渲染问题。纹理也可能有采样问题,但它们发生在正在渲染的三角形的内部。
像素着色器通过将纹理坐标值传递给texture2D之类的调用来访问纹理。这些值在(u,v)纹理坐标,由对应函数映射到范围[0.0,1.0]。GPU负责将这个值转换为texel坐标。在不同的api中,纹理坐标系之间有两种主要的区别。在DirectX中,纹理的左上角为(0,0),右下角为(1,1)。这与有多少图像类型存储它们的数据相匹配,第一行是文件中的第一个。在OpenGL中,(0,0)位于左下角,y轴对比DirectX是翻转的。texel具有整数坐标,但我们通常希望访问texel之间的一个位置并在其中进行混合。这就引出了一个问题:像素中心的点坐标是什么。Heckbert讨论了两种可能的系统:截断(truncating)和舍入(rounding)。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。
这里值得解释的一个术语是依赖纹理读取(dependent texture read),它有两个定义。第一种尤其适用于移动设备。当通过texture2D或类似的方法访问纹理时,当像素着色器计算纹理坐标而不是使用顶点着色器传入的未经修饰的纹理坐标时,就会发生依赖的纹理读取。注意,这意味着对传入的纹理坐标进行任何更改,即使是交换u和v值这样的简单操作。旧的移动gpu,那些不支持OpenGL ES 3.0的gpu,在着色器没有依赖纹理读取时运行得更好,因为可以预先获取texel数据。这个术语的另一个更古老的定义对于早期的台式机gpu尤其重要。在这个上下文中,当一个纹理的坐标依赖于之前一些纹理值的结果时,就会发生依赖纹理读取。例如,一个纹理可能会更改着色法线,这反过来又会更改用于访问cube map的坐标。这种功能在早期的gpu上是有限的,甚至是不存在的。今天,这种读取可能会影响性能,这取决于在批处理中计算的像素数量以及其他因素。有关更多信息,请参见第23.8节。
gpu中使用的纹理图像大小通常为$2^m/times2^n$个纹理,其中m和n是非负整数。这些被称为2的幂纹理(power-of-two ,POT)。现代gpu可以处理任意大小的非二幂(NPOT)纹理,这允许将生成的图像作为纹理处理。然而,一些较老的移动gpu可能不支持NPOT纹理的mipmapping(第6.2.2节)。图形加速器对纹理大小有不同的上限。例如,directx12最多允许$16384^2$个texel。
假设我们有一个大小为256×256的纹理,并且我们想把它用作正方形上的纹理。只要投影在屏幕上的正方形与纹理大小大致相同,那么正方形上的纹理看起来就几乎与原始图像一样。但是,如果投影的正方形所包含的像素是原始图像的十倍(称为放大),或者投影的正方形只覆盖屏幕的一小部分(称为缩小),会发生什么呢?答案是,这取决于您决定对这两种不同的情况使用哪种采样和筛选方法。
本章讨论的图像采样和过滤方法适用于从每个纹理中读取的值。然而,期望的结果是防止最终渲染的图像中的锯齿,这在理论上需要采样和过滤最终像素的颜色。这里的区别是过滤着色方程的输入,还是过滤输出。只要输入和输出是线性相关的(这对于输入来说是正确的,比如颜色),那么过滤单独的纹理值就相当于过滤最终的颜色。然而,许多存储在纹理中的着色器输入值,如表面法线和粗糙度值,与输出有非线性关系。标准的纹理过滤方法可能对这些纹理不起作用,导致锯齿。第9.13节讨论了过滤这些纹理的改进方法。
2.1放大
在图6.8中,一个大小为48×48 texels的纹理被纹理化到一个正方形上,并且这个正方形相对于纹理大小更大,因此底层的图形系统必须放大纹理。放大最常用的滤波技术是最近邻(实际的滤波器称为框式滤波器——参见5.4.1节)和双线性插值。还有三次卷积,它使用4×4或5×5个纹理数组的加权和。这使得更高的放大质量。虽然本地硬件支持三次卷积(也称为双三次插值bicubic interpolation)目前并不常见,但它可以在着色器程序中执行。
原书图6.8,纹理放大48×48图像到320×320像素。左:最近邻滤波,每个像素选择最近的图素。中间:双线性滤波使用加权平均的四个最近的图素。右:使用5×5个最接近的图素的加权平均值进行三次滤波。
在图6.8的左侧,使用了最近邻方法。这种放大技术的一个特点是单个纹理可能变得明显。这种效果称为像素化,之所以会发生这种情况,是因为该方法在放大时取距离每个像素中心最近的texel的值,从而产生块状外观。虽然这种方法的质量有时很差,但它只需要为每个像素获取一个texel。
在同一图的中间图像中,使用双线性插值(有时称为线性插值)。对于每个像素,这种滤波方法对相邻的四个纹理进行插值,并在二维空间中进行线性插值,得到像素的混合值。结果更加模糊,使用最近邻方法产生的锯齿已经消失。作为一个实验,试着在眯眼的时候看左边的图片,因为这和低通滤波器的效果差不多,而且更能显示脸部。
回到之前的砖块纹理示例:在不降低分数的情况下,我们得到(pu,pv) = (81.92,74.24)。我们在这里使用OpenGL的左下角原点texel坐标系,因为它与标准的笛卡尔坐标系相匹配。我们的目标是在四个最近的texel之间插入,使用它们的texel中心来定义一个texel大小的坐标系统。参见图6.9。要找出最近的四个像素,我们要减去像素中心分数(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’)。
原书图6.9,双线性插值。所涉及的四个texel由左边的四个正方形表示,texel中心为蓝色。右边是由四个texel的中心组成的坐标系。
将纹理访问函数定义为t(x,y),其中x和y是整数,返回texel的颜色。任意位置(u’,v’)可以计算为一个两步过程。首先,底部的texels, t(x,y)和t (x + 1,y),水平插值(使用u’),对于最上面的两个texel也类似,t(x,y + 1)和t(x + 1,y + 1)。对于底部的texel,我们得到(1-u’)t(x,y) + u’t(x + 1,y)(图6.9中底部的绿色圆圈),对于顶部(1-u’)t (x,y + 1) + u’t(x + 1,y + 1)(顶部绿色圆圈)。然后将这两个值垂直插值(使用v’),因此双线性插值的颜色b$(p_u,p_v)$为
直观地说,靠近示例位置的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。
对于伴随放大而来的模糊,一个常见的解决方案是使用细节纹理(detail textures)。这些纹理代表了新的表面细节,从手机上的划痕到地形上的灌木丛。这样的细节被叠加到放大的纹理上,作为一个单独的纹理,在不同的尺度上。细节纹理的高频重复图案,结合低频放大纹理,具有类似于使用单一高分辨率纹理的视觉效果。
双线性插值在两个方向上线性插值。然而线性插值不是必需的。例如,一个纹理由棋盘图案中的黑白像素组成。使用双线性插值可以得到不同灰度的纹理样本。通过重新映射,例如,所有低于0.4的灰色都是黑色的,所有高于0.6的灰色都是白色的,而那些介于两者之间的灰色被拉伸来填补空白,纹理看起来更像一个棋盘格,同时也给了一些纹理之间的混合。参见图6.10。
原书图6.10,最近邻方法,双线性插值,以及使用相同的2×2个棋盘纹理重新映射部分的方式。注意,由于纹理和图像网格并不完全匹配,因此最近邻采样给出的正方形大小略有不同。
使用高分辨率纹理也有类似的效果。例如,假设每个检查器正方形由4×4个texel组成,而不是1×1。围绕每个检查器的中心,插入的颜色将完全是黑色或白色。
在图6.8的右边,使用了一个双三次滤波器,剩余的块度基本上被去掉了。值得注意的是,双三次滤波器比双线性滤波器消耗高。然而,许多高阶滤波器可以表示为重复线性插值(也参见第17.1.1节)。因此,纹理单元中用于线性插值的GPU硬件可以通过几个查找来使用。
如果双三次滤波器被认为过于昂贵,Quilez提出了一种简单的技术,使用平滑曲线在一组2×2个texels之间插入。我们首先描述曲线,然后是技术。两种常用的曲线是平滑步长曲线(smoothstep curve)和五次曲线(quintic curve):
当您想要平滑地从一个值插入到另一个值时,这些方法非常有用。平滑步长曲线具有s’(0)=s’(1)=0的性质,在0和1之间是光滑的。五次曲线具有相同的性质,但q”(0)=q”(1)=0,即二阶导数在曲线的起点和终点也是0。这两条曲线如图6.11所示:
原书图6.11,平滑步长曲线s(x)(左)和五次曲线q(x)(右)。
该技术首先计算(u‘,v’)(与公式6.1和图6.9中使用的方法相同),首先将样本乘以纹理尺寸并添加0.5。整数部分保留到后面,分数存储在u‘和v’中,范围在[0,1]。将(u’,v’)变换$(t_u,t_v)=(q(u’),q(v’))$,仍然在[0,1]。最后减去0.5,再把整数部分加回去;得到的u坐标然后除以纹理宽度,v也是一样。此时,新的纹理坐标与GPU提供的双线性插值查找一起使用。注意,这个方法将在每个texel处给出一个平台,这意味着,如果texel位于RGB空间中的一个平面上,那么这种类型的插值将会给出一个光滑的,但仍然是阶梯状的外观,这可能并不总是需要的。参见图6.12。
原书图6.12,四种不同的方法来放大一维纹理。橙色圆圈表示texel的中心以及texel值(高度)。从左到右:最近邻,线性,使用每对相邻的纹理之间的五次曲线,使用三次插值。
2.2缩小
当纹理最小化时,几个纹理可能覆盖一个像素的单元格,如图6.13。得到每个像素的正确颜色值,你应该整合纹理对像素的影响。然而,要精确地确定一个特定像素附近的所有texels的确切影响是困难的,而且实际上不可能实时完美地做到这一点。
原书图6.13,缩小:通过一排像素单元格显示棋盘格纹理正方形的视图,大致显示了一些纹理如何影响每个像素。
因为这个限制,CPUs上使用了几种方法。一种方法是使用最邻近法,工作原理与相应的放大滤波器完全相同,即它选择在像素单元的中心可见的texel。这种滤波器可能会导致严重的锯齿问题。在图6.14中,最上面的图使用了最近邻法。在水平方向上,由于选择了表示一个像素的众多纹理中的一个来表示表面,所以出现了伪影。当表面相对于观察者移动时,这样的伪影更加明显,并且是所谓的时间锯齿(temporal aliasing)的一种表现。
原书图6.14,顶部图像使用点采样(最近邻)渲染,中间使用mipmapping渲染,底部使用求和面积表渲染。
另一种常用的滤波器是双线性插值,其工作原理与放大滤波器完全相同。这个滤波器只比最小化的最近邻方法稍微好一点。它混合了四个纹理而不是仅仅使用一个,但是当一个像素受到四个以上的纹理的影响时,滤波器很快就会失效并产生锯齿。
更好的解决方案是可能的。如5.4.1节所述,锯齿问题可以通过采样和滤波技术来解决。纹理的信号频率取决于其纹理在屏幕上的间距。由于Nyquist限制,我们需要确保纹理的信号频率不大于采样频率的一半。例如,假设一个图像是由交替的黑白线组成的,中间有一个texel。然后波长是两个texel宽(从黑线到黑线),所以频率是1/2。若要在屏幕上正确显示此纹理,则频率必须至少为1/2×2,即,每个texel至少有一个像素。所以,对于一般的纹理,每个像素最多应该有一个texel来避免锯齿。
为了达到这个目的,要么提高像素的采样频率,要么降低纹理频率。前一章讨论的抗锯齿方法给出了提高像素采样率的方法。然而这些只有限的增加采样频率。为了更好地解决这一问题,各种纹理细化算法应运而生。
所有纹理抗锯齿算法的基本思想都是一样的:预处理纹理并创建数据结构,这些数据结构将帮助计算一组纹理在像素上的效果的快速近似。对于实时工作,这些算法的特点是使用固定的时间和资源执行。以这种方式,每个像素使用固定数量的样本并组合以计算(可能大量)texels的效果。
Mipmapping
最流行的纹理抗锯齿方法叫做mipmapping。它以某种形式在现在生成的所有图形加速器上实现。“Mip”在拉丁语中是“很多东西在一个小地方”的意思,这里指将原始纹理反复过滤成更小的图像的过程。
当使用mipmapping最小化过滤器时,在实际渲染之前,原始纹理会被一组更小版本的纹理替代。纹理(在0级)被向下采样到原始区域的四分之一,每个新的texel值通常计算为原始纹理中四个相邻texel的平均值。新的一级纹理有时被称为原始纹理的子纹理。递归地执行缩减,直到纹理的一个或两个维度都等于一个texel。这个过程如图6.15所示。作为一个整体的图像集通常被称为mipmap chain。
原书图6.15,一个mipmap是通过获取原始图像(0级),在金字塔的底部,将每个2×2个区域平均为下一层的texel值而形成的。纵轴是第三个纹理坐标d,在图中d不是线性的;它是一种度量方法,用来度量一个样本用于插值的两个纹理级别。
形成高质量mipmap的两个重要因素是良好的滤波和伽马校正。形成mipmap级别的常用方法是取每2×2组texel的平均值,以获得mip texel值。使用的盒型过滤器可能是最差的过滤器之一。这可能会导致质量很差,因为它会不必要地模糊低频,同时保留一些导致锯齿的高频。最好使用高斯、Lanczos、Kaiser或类似的过滤器。一些api支持GPU本身更好的滤波。在纹理的边缘附近,过滤时必须注意纹理是重复的还是单一的复制。
对于非线性空间中编码的纹理(如大多数颜色纹理),在过滤时忽略gamma校正将修改mipmap级别的感知亮度。当你离物体越远,使用未校正的mipmap,物体整体看起来就越暗,对比度和细节也可以被改变。因此,将这些纹理从sRGB转换为线性空间(第5.6节),在该空间中执行所有mipmap过滤,并将最终结果转换回sRGB颜色空间进行存储是非常重要的。大多数api都支持sRGB纹理,因此可以在线性空间中正确生成mipmap并将结果存储在sRGB中。当访问sRGB纹理时,它们的值首先被转换到线性空间,以便正确地执行放大和缩小。
正如前面提到的,一些纹理与最终的着色颜色有着非线性关系。虽然这在一般情况下会造成过滤问题,但是mipmap生成对这个问题特别敏感,因为要过滤成百上千个像素。为了获得最佳结果,通常需要使用专门的mipmap生成方法。这些方法详见第9.13节。
纹理化时访问此结构的基本过程很简单。 屏幕像素包围纹理本身的区域。当像素的区域投射到纹理上时(图6.16),它包含一个或多个纹理。使用像素的单元格边界并不完全正确,但是这里使用它是为了简化表示。单元格外部的texels可以影响像素的颜色如5.4.1之前看到的部分。我们的目标是粗略地确定像素中有多少纹理。有两种常用的计算方法用于计算d(OpenGL称为$/lambda$,也称为细节的纹理级别texture level of detail)。一种是利用像素单元形成的较长的四边形边缘来近似像素的覆盖范围;另一种方法是使用四个不同值($/partial u//partial x,/partial v//partial x,/partial u//partial y,/partial u//partial y$)中的最大绝对值作为度量。每个差分是纹理坐标相对于屏幕轴的变化量的度量。例如,$/partial u//partial x$是一个像素的u纹理值沿屏幕x轴的变化量。有关这些方程的更多信息,请参见Williams的原始文章或Flavell的文章或Pharr的文章。McCormack等人用最大绝对值法讨论了锯齿的引入,他们给出了一个替代公式。Ewins等分析了几种质量相当的算法的硬件成本。
原书图6.16,在左边是一个正方形像素单元和它的纹理视图。右边是像素单元在纹理本身上的投影。
这些梯度值可用于使用shader Model 3.0或更新版本的像素着色器程序。因为它们是基于相邻像素值之间的差异,它们在受动态流控制影响的像素着色器部分不可访问(第3.8节)。对于要在这样的部分中执行的纹理读取(例如,在循环中),必须更早地计算导数。注意,由于顶点着色器不能访问梯度信息,梯度或细节级别需要在顶点着色器本身计算,并在使用顶点纹理时提供给GPU。
计算坐标d的目的是确定沿着mipmap的金字塔轴在哪里采样。参见图6.15。目标是像素与texel的比例至少为1:1,以实现奈奎斯特速率。这里的重要原则是,当像素单元包含更多的纹理和d时,将访问一个更小、更模糊的纹理版本。(u,v,d) 用于访问mipmap。值d类似于纹理级别,但它不是一个整数值,而是级别之间距离的分数值。对d位置上的纹理层和下的纹理层进行采样。(u,v) 用于从这两个纹理级别中的每一个检索双线性插值样本。然后根据每个纹理层到d的距离,对得到的样本进行线性插值。整个过程称为三线性插值,每像素执行一次。
d坐标上的一个用户控件是细节偏差级别(LOD偏差)。这是一个添加到d的值,因此它影响纹理的相对感知锐度。如果我们进一步向上移动金字塔开始(增加d),纹理将看起来更加模糊。对于任何给定的纹理,良好的LOD偏差都会随着图像类型和使用方式的不同而变化。例如,一开始有些模糊的图像可能使用负偏置,而用于纹理处理的低分辨率(别名)合成图像可能使用正偏置。可以指定纹理的整体偏置,或者像素着色器中的每个像素偏置。为了更好的控制,用户可以提供用于计算它的d坐标或导数。
mipmapping的好处在于,它不是试图单独地对每个像素的所有图素进行求和,而是访问和插值预组合的图素集。这个过程需要固定的时间,无论缩小多少。然而,mipmapping有几个缺陷。一个主要问题是过度模糊。假设一个像素单元在u方向上覆盖了大量的图素,而在v方向上只覆盖了少量图素。这种情况通常发生在查看器沿纹理表面几乎是边缘朝上看时。事实上,可能需要沿着纹理的一个轴进行缩小,然后沿着另一个轴进行放大。访问mipmap的方法是检索纹理上的正方形区域;检索矩形区域是不可能的。为了避免锯齿,我们选择纹理上像素单元的近似覆盖率的最大度量。这导致检索到的样本通常比较模糊。这方面可以在图6.14的mipmap图像中看到。向右移动的线显示出过度模糊。
Summed-Area Table 区域求和表技术
避免过度模糊的另一种方法是区域求和表(SAT)。要使用此方法,首先创建一个与纹理大小相同的数组,但包含存储颜色的更多精度(例如,红色、绿色和蓝色各16位或更多)。在该数组的每个位置,必须计算并存储由该位置和texel (0,0)(原点)。在这个数组的每个位置,必须计算并存储由这个位置和texel(0,0)组成的矩形中所有对应纹理的texel的和。在纹理处理过程中,像素单元在纹理上的投影被一个矩形绑定。然后访问区域求和表来确定这个矩形的平均颜色,该颜色作为像素的纹理颜色传递回来。用图6.17所示矩形的纹理坐标计算平均值。这是利用式6.3所给出的公式来完成的:
这里,x和y是矩形的texel坐标,s[x,y]是该texel的求和面积值。这个方程的计算方法是从右上角到原点的整个面积的和,然后减去邻边角的面积A和B。面积C减去了两次,所以它是由左下角加回来的。注意$(x_{ll},y_{ll})$为C区域的右上角,即$(x_{ll}+1,y_{ll}+1)$是边界框的左下角。
原书图6.17,将像素单元反投影到纹理上,由矩形绑定;矩形的四个角用于访问区域求和表。
使用区域求和表的结果如图6.14所示。接近地平线的线在右边边缘更清晰,但是中间的对角线交叉线仍然过于模糊。问题是,当一个纹理沿着它的对角线进行观察时,会生成一个大的矩形,其中许多纹理都不位于正在计算的像素附近。例如,想象一个长而细的矩形,表示像素单元的背投影位于图6.17中整个纹理的对角线上。将返回整个纹理矩形的平均值,而不仅仅是像素单元内的平均值。
求和面积表是各向异性滤波算法的一个例子。这种算法在非正方形的区域上检索texel值。然而,SAT能够在主要水平和垂直方向上最有效地做到这一点。还请注意,对于大小为16×16以下的纹理,和区域表占用的内存至少是其两倍,对于较大的纹理,需要更精确的内存。
可以在现代gpu上实现区域求和表,它以合理的总体内存成本提供更高的质量。改进的滤波对高级渲染技术的质量至关重要。例如,Hensley等人提供了一个有效的实现,并展示了面积采样是如何提高光泽度的。使用面积采样的其他算法可以通过SAT得到改进,比如景深、阴影映射和模糊反射。
Unconstrained Anisotropic Filtering 无约束各向异性过滤
对于当前的图形硬件,进一步改进纹理过滤最常用的方法是重用现有的mipmap硬件。其基本思想是将像素单元反投影,然后对纹理上的这个四边形(quad)进行多次采样,并将这些采样组合在一起。如上所述,每个mipmap示例都有一个位置和一个与之相关的方形区域。该算法不是使用单一的mipmap样本来近似这个四边形的覆盖范围,而是使用几个正方形来覆盖这个四边形。短边的四分之一可以用来确定d(不像在mipmapping中,通常使用较长的边);这使得每个mipmap样本的平均面积更小(因此更不模糊)。四边形的长边用于创建一条各向异性线,平行于长边并穿过四边形的中间。当各向异性的数量在1:1和2:1之间时,沿着这条线取两个样本(见图6.18)。在各向异性比例较高的情况下,沿轴取的样品较多。
原书图6.18,各向异性过滤。像素单元的后投影创建一个四边形。较长的边之间形成一条各向异性线。
该方案允许各向异性线在任意方向运行,不受面积和表的限制。它也不需要比mipmap更多的纹理内存,因为它使用mipmap算法进行采样。各向异性滤波的一个例子如图6.19所示。
原书图6.19,Mipmap与各向异性滤波。左边做了三线性mipmapping,右边做了16:1各向异性滤波。水平方向,各向异性滤波提供了一个更清晰的结果,最小的锯齿。
这种沿轴采样的想法最初是由Schilling等人用他们的Texram动态存储设备提出的。Barkans描述了算法在Talisman系统中的使用。McCormack等人提出了一种类似的系统,称为Feline。Texram的原始公式中,各向异性轴(也称为探针)上的样品具有相同的权重。Talisman 赋予轴两端的两个探针一半的重量。Feline 使用高斯滤波来对一组探针进行加权。这些算法接近软件采样算法的高质量,如椭圆加权平均(EWA) 滤波算法,该算法将像素的影响区域在纹理上转换为一个椭圆,并用滤波内核对椭圆内的纹理进行加权。Mavridis和Papaioannou介绍了几种在GPU上使用着色器代码实现EWA滤波的方法。
2.3体积纹理
图像纹理的一个直接扩展是由(u,v,w)或(s,t,r)。例如,医学成像数据可以生成一个三维网格。通过在网格中移动一个多边形,可以查看这些数据的二维切片。一个相关的想法是用这种形式表示体积光。在一个表面上的一个点上的照明是通过找到它在这个体积内的位置的值,结合光的方向来发现的。
大多数gpu支持体积纹理(volume textures)的mipmapping。由于在体纹理的单个mipmap层中进行滤波涉及到三线性插值,因此在mipmap层之间进行滤波需要进行四线性插值。由于这涉及到对16个纹理的结果进行平均,可能会产生精度问题,可以通过使用更高精度的体纹理来解决。Sigg和Hadwiger讨论了这个和其他与体纹理相关的问题,并提供了执行过滤和其他操作的有效方法。
虽然体纹理有明显更高的存储要求和更昂贵的过滤,他们确实有一些独特的优势。由于三维位置可以直接用作纹理坐标,因此可以跳过为三维网格确定良好的二维参数的复杂过程。这避免了通常使用二维参数化出现的失真和接缝问题。体纹理还可以用来表示木材或大理石等材料的体结构。
使用体纹理进行表面纹理处理是非常有效的,因为绝大多数样本都没有使用。Benson和Davis以及DeBry等讨论了在稀疏八叉树结构中存储纹理数据的问题。该方案与交互式三维绘画系统很好地配合,因为曲面在创建时不需要指定明确的纹理坐标,并且八叉树可以将纹理细节控制在任何需要的水平。Lefebvre等讨论了在现代GPU上实现八叉树纹理的细节。Lefebvre和Hoppe讨论了一种将稀疏的体积数据打包成明显更小的纹理的方法。
2.4立方体纹理
另一种类型的纹理是立方体纹理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)。Cube maps对于表示方向函数的值很有用;它们最常用于环境映射(第10.4.3节)。
2.5纹理表示
在应用程序中处理许多纹理时,有几种方法可以提高性能。纹理压缩在第6.2.6节中进行了描述,而本节的重点是纹理地图集(texture atlases)、纹理数组(texture arrays)和无绑定纹理(bindless textures),所有这些都是为了避免在呈现时更改纹理的成本。在19.10.1和19.10.2节中,描述了纹理流和代码转换。
为了能够为GPU批量处理尽可能多的工作,通常倾向于尽可能少地更改状态(第18.4.2节)。为此,可以将多个图像放入一个更大的纹理中,称为纹理图集。这在图6.20的左边进行了说明。注意,子纹理的形状可以是任意的,如图6.6所示。Noll和Stricker描述了子纹理布局地图集的优化。还需要注意mipmap的生成和访问,因为mipmap的上层可能包含几个独立的、不相关的形状。Manson和Schaefer提出了一种考虑曲面参数化的优化mipmap创建方法,该方法可以产生更好的结果。Burley和Lacewell提出了一个叫做Ptex的系统,在这个系统中,细分表面上的每个四边形都有自己的小纹理。这样做的好处是避免了在网格上分配唯一的纹理坐标,并且在纹理图集的断开部分的接缝上没有伪像。为了能够跨四元组过滤,Ptex使用邻接数据结构。当最初的目标是生产渲染时,Hillesland呈现了压缩的Ptex,它将每个人脸的子纹理放入一个纹理图集,并使用相邻人脸的填充来避免过滤时的间接性。Yuksel提出了网格颜色纹理,它改进了Ptex。Toth通过实现一种方法,即在超出范围$[0,1]^2$的情况下滤波器被丢弃,为Ptex类系统提供高质量的面部过滤。
原书图6.20,左图:一个纹理图集,其中9张较小的图像被合成为一个较大的纹理。右图:更现代的方法是将较小的图像设置为纹理数组,这在大多数api中都是一个基本概念。
使用图集的一个难点是换行/重复和镜像模式,这不会正确地影响子纹理,只会影响整个纹理。另一个问题可能发生在为一个图集生成mipmaps时,其中一个子纹理可能渗入另一个子纹理。但是,在将每个子纹理放入大型纹理图集并对子纹理使用2的幂次分辨率之前,可以分别为每个子纹理生成mipmap层次结构,从而避免这种情况。
解决这些问题的一个更简单的方法是使用称为纹理数组的API构造,它完全避免了mipmapping和重复模式的任何问题。参见图6.20的右边部分。纹理数组中的所有子纹理都需要具有相同的维度、格式、mipmap层次结构和MSAA设置。和纹理图集一样,设置一个纹理数组只需要执行一次,然后可以使用着色器中的索引访问任何数组元素。这比绑定每个子纹理快5倍。
还可以帮助避免状态更改成本的一个特性是对无绑定纹理的API支持。如果没有无绑定纹理,则使用API将纹理绑定到特定的纹理单元。一个问题是纹理单元数量的上限,这使程序员的工作变得复杂。驱动程序确保纹理驻留在GPU端。对于无绑定的纹理,没有纹理数量的上限,因为每个纹理仅由64位指针(有时称为句柄handle)与其数据结构关联。这些句柄可以通过许多不同的方式访问,例如,通过uniforms,通过不同的数据,从其他纹理,或从着色器存储缓存对象(SSBO)。应用程序需要确保纹理驻留在GPU端。无绑定纹理避免了驱动程序中的任何类型的绑定成本,这使得渲染速度更快。
2.6纹理压缩
一个直接应对内存和带宽问题以及缓存问题的解决方案是固定速率纹理压缩(texture compression)。通过GPU实时解码压缩纹理,纹理可以占用更少的纹理内存,从而增加有效的缓存大小。同样重要的是,这样的纹理使用起来更有效,因为它们在访问时消耗的内存带宽更少。一个相关但不同的原因是为了提供更大的纹理而添加压缩。例如,在$512^2$分辨率下,每个texel使用3个字节的非压缩纹理将占用768 kB。使用纹理压缩,压缩比为6:1,$1024^2$纹理只占用512 kB。
图像文件格式(如JPEG和PNG)中使用了多种图像压缩方法,但是在硬件中实现对这些方法的解码非常昂贵(关于纹理转码的信息,请参阅19.10.1节)。S3开发了一种称为S3纹理压缩(S3TC)的方案,该方案被选为DirectX的标准,在DirectX中称为DXTC,在DirectX 10中称为BC (Block Compression)。此外,它是OpenGL中的标准,因为几乎所有gpu都支持它。它的优点是创建一个大小固定、独立编码的压缩图像,并且解码简单(因此也快速)。图像的每个压缩部分都可以独立于其他部分处理。没有共享查找表或其他依赖项,这简化了解码。
DXTC/BC压缩方案有七种变体,它们具有一些共同的特性。编码是在4×4 texel块上完成的,也称为tiles。每个块分别编码。编码基于插值。对于每个编码的量,存储两个参考值(例如,颜色)。为块中的16个texel中的每个保存一个插值因子。它在两个参考值之间的直线上选择一个值,例如,一个颜色等于或从两个存储的颜色中插入。压缩来自于仅存储两种颜色以及每个像素的短索引值。
在表6.1中总结了这7种变体之间的精确编码。注意,“DXT“指示DirectX 9中的名称和”BC”指示DirectX 10及以上的名称。从表中可以看出,BC1有两个16位参考RGB值(5位红色,6位绿色,5位蓝色),每个texel都有一个2位插值因子,可以从一个参考值或两个中间值中选择(另一种DXT1模式为透明像素保留了四种可能的插值因子之一,将插值的数量限制为三个——两个参考值及其平均值。)。这表示纹理压缩比为6:1,与未压缩的24位RGB纹理相比。BC2以与BC1相同的方式编码颜色,但是每个texel增加4位(bpt)用于量子化(raw) alpha。对于BC3,每个块都以与DXT1块相同的方式编码RGB数据。此外,alpha数据使用两个8位参考值和一个3位插值因子进行编码。每个texel可以选择一个参考alpha值或六个中间值中的一个。BC4有一个通道,在BC3中编码为alpha。BC5包含两个通道,其中每个通道编码为BC3。
表6.1,纹理压缩格式。所有这些压缩块都是4×4texels。存储列显示每个块的字节数(B)和每个texel的比特数(bpt)。参考颜色的符号后为每个通道的比特数。例如RGB565表示红色和蓝色通道有5位,而绿色通道有6位。
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)被纳入API。该方案与S3TC具有解码速度快、随机存取、无间接查找、速率固定等特点。它将一个由4×4个texel组成的块编码为64位,即,每个texel使用4位。基本思想如图6.21所示。每个2×4块(或4×2块,取决于哪个块的质量最好)存储一个基本颜色。每个块还从一个小型静态查找表中选择一组四个常量,块中的每个texel都可以选择添加该表中的一个值。这修改了每个像素的亮度。图像质量与DXTC相当。
原书图6.21,ETC (Ericsson texture compression)编码像素块的颜色,然后修改每个像素的亮度,以创建最终的texel颜色。
在ETC2中,OpenGL ES 3.0利用未使用的位组合,为原有的ETC算法添加了更多的模式。未使用的位组合是压缩表示(例如,64位),它解压缩为与另一个压缩表示相同的图像。例如,在BC1中,将两个引用颜色设置为相同是没有用的,因为这将表示一个常量颜色块,而只要一个引用颜色包含该常量颜色,就可以得到该常量颜色块。在ETC中,一种颜色也可以从具有带符号数的第一种颜色进行增量编码,因此该计算可以上溢或下溢。这种情况被用来表示其他压缩模式。ETC2增加了两种新的模式,每种模式有四种颜色,每种颜色的派生方式不同,最后一种模式是RGB空间中的一个平面,用于处理平滑的转换。Ericsson alpha compression (EAC)压缩只有一个组件的图像。这种压缩类似于基本ETC压缩,但只针对一个组件,生成的图像每个texel存储4位。它可以选择与ETC2结合使用,此外,还可以使用两个EAC通道来压缩法线(更多内容见下面的主题)。所有ETC1、ETC2和EAC都是OpenGL 4.0核心概要文件、OpenGL ES 3.0、Vulkan和Metal的一部分。
压缩法线贴图(在第6.7.2节中讨论)需要注意。为RGB颜色设计的压缩格式通常不适用于普通的xyz数据。大多数方法都利用了法线已知为单位长度这一事实,并进一步假设其z分量为正(对于切线空间法线来说,这是一个合理的假设)。这只允许存储法线的x和y分量。z分量是动态地派生出来的:
这本身会导致适度的压缩,因为只存储两个组件,而不是三个。由于大多数gpu本身并不支持三组件纹理,这也避免了浪费组件的可能性(或者必须在第四个组件中打包另一个数量)。进一步的压缩通常通过以BC5 / 3Dc格式的纹理存储x和y组件来实现。参见图6.22。由于每个块的参考值限定了最小和最大的x和y分量值,因此可以将它们看作是xy平面上的一个边界框。3位插值因子允许在每个轴上选择8个值,因此边界框被划分为一个包含可能法线的8×8网格。或者,可以使用EAC的两个通道(用于x和y),然后按照上面的定义计算z。
原书图6.22,左:球体上的法线单位只需要编码x和y分量。右:对于BC4/3Dc, xy平面上的一个方框包含法线,该方框内的8×8个法线可以用于每4×4块法线(为了清晰起见,这里只显示4×4个法线)。
在不支持BC5/3Dc或EAC格式的硬件上,一个常见的退路是使用DXT5格式纹理,并将两个组件存储为绿色和alpha组件(因为这些组件的存储精度最高)。另外两个组件未使用。
PVRTC是一种纹理压缩格式,可以在Imagination Technologies的硬件PowerVR上使用,它最广泛的应用是在iphone和ipad上。它为每个texel提供了一个2位和4位的方案,并压缩了4×4个texel的块。其关键思想是提供图像的两个低频(平滑)信号,这两个信号是通过相邻的texel数据块和插值得到的。然后,使用每个texel1或2位对图像上的两个信号进行插值。
Adaptive scalable texture compression (ASTC)的不同之处在于它将一块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提出了几种提高压缩纹理质量的方法。对于包含颜色和法线贴图的纹理,建议使用每个组件16位元来创建贴图。对于颜色纹理,然后执行直方图重正化(对这16位),然后在着色器中使用比例和偏置常数(每个纹理)反转直方图的效果。直方图归一化是将图像中使用的值分散到整个范围内的一种技术,它是对比度增强的一种。每个组件使用16位元,可以确保直方图在重新正规化之后没有未使用的位置,这减少了许多纹理压缩方案可能引入的带状伪迹。如图6.23所示。此外,Kaplanyan建议,如果75%的像素在116/255以上,那么纹理使用线性颜色空间,否则将纹理存储在sRGB中。对于法线贴图,他还指出BC5/3Dc经常独立于y压缩x,这意味着并不总是能找到最好的法线。相反,他建议对法线使用以下误差度量:
其中n为原始法线,$n_c$为相同的法线压缩后再解压缩。
原书图6.23,在纹理压缩过程中,每个组件使用16位而不是8位的效果。从左到右:原始纹理,DXT1从每个组件8位压缩而成,DXT1从每个组件16位压缩而成,在着色器中重新正规化。为了更清晰地显示效果,纹理被渲染为强烈的灯光。
需要注意的是,在不同的颜色空间中也可以压缩纹理,这可以用来加速纹理压缩。一个常用的转换是RGB→YCoCg:
其中Y是亮度项Co和Cg是色度项。逆变换也很简单:
这相当于增加了一些内容。这两个变换是线性的,从方程6.6可以看出,它是一个矩阵-向量乘法,它本身是线性的(见方程4.1和4.2)。这很重要,因为可以存储YCoCg,而不是在纹理中存储RGB;纹理硬件仍然可以在YCoCg空间中执行过滤,然后像素着色器可以根据需要转换回RGB。应该注意的是,这种转换本身可能是有损的,也可能无关紧要。
还有另一个可逆的RGB→YCoCg变换,总结为:
$/gg$表示右移位,这意味着可以在24位RGB颜色和相应的YCoCg表示之间来回转换,而不会有任何损失。需要注意的是,如果RGB中的每个分量都有n位元,那么Co和Cg都有n + 1位元,以保证可逆变换;不过Y只需要n位。Van Waveren和Castano使用有损YCoCg转换在CPU或GPU上实现对DXT5/BC3的快速压缩。他们将Y存储在alpha通道中(因为它的精度最高),而Co和Cg存储在RGB的前两个组件中。压缩变得很快,因为Y是单独存储和压缩的。对于Co-和Cg-组件,它们找到一个二维边界框并选择产生最佳结果的框对角线。注意,对于在CPU上动态创建的纹理,最好也在CPU上压缩纹理。当纹理通过GPU渲染创建时,通常最好也在GPU上压缩纹理。YCoCg变换和其他亮度-色度变换常用于图像压缩,其中色度分量的平均值超过2×2个像素。这就减少了50%的存储空间,而且由于色度变化比较慢,所以通常不会起作用。Lee-Steere和Harmon进一步将其转换为色调饱和度值(hue-saturation-value , HSV),在x和y中将色调和饱和度的采样值降低4倍,并将值存储为单个通道DXT1纹理。Van Waveren和Castano也描述了法线贴图的快速压缩方法。
Griffin和Olano的研究表明,当多个纹理应用于具有复杂着色模型的几何模型时,纹理的质量往往很低,没有任何可感知的差异。因此,根据用例,降低质量是可以接受的。Fauconneau提出了一种针对directx11纹理压缩格式的SIMD实现。
3程序纹理
给定纹理空间位置,执行图像查找是生成纹理值的一种方法。另一种方法是对函数求值,从而定义程序化纹理。
程序化纹理在离线渲染中很常用,而图像纹理在实时渲染中要更常见。这是由于现代gpu中图像纹理处理硬件有极高效率,可以在一秒钟内执行数十亿次纹理访问。然而,GPU架构正在向更便宜的计算和(相对)更昂贵的内存访问发展。这些趋势使得程序化纹理在实时应用中得到了更大的应用。
考虑到体纹理的高存储成本,体纹理是一个特别有吸引力的程序纹理应用。这种纹理可以通过多种技术合成。最常见的一种方法是使用一个或多个噪声函数来生成值。参见图6.24。噪声函数通常以连续的2次幂频率采样,称为八度(octaves)。每个八度都有一个权值,通常随着频率的增加而下降,这些加权样本的和称为湍流函数(turbulence function)。
原书图6.24,两个使用体纹理的实时程序纹理示例。左边的大理石是半透明的体纹理,使用光线步进(ray marching)。右边的对象是一个合成图像,使用一个复杂的程序木着色器生成,并在一个真实的环境中合成。
由于计算噪声函数的代价较大,三维阵列中的格点通常是预先计算好的,用来插值纹理值。有多种方法可以使用颜色混合来快速生成这些数组。Perlin提出了一种快速、实用的采样该噪声函数的方法,并展示了一些用途。Olano提供了噪声生成算法,允许在存储纹理和执行计算之间进行权衡。McEwan等人开发了不需要任何查找就可以计算着色器中的经典噪声和单纯噪声的方法,并提供了源代码。Parberry使用动态规划将计算分摊到几个像素上,以加速噪声计算。Green提供了一种更高质量的方法,但它更适合于近交互应用程序,因为它使用50像素着色器指令进行一次查找。可以对Perlin提出的原始噪声函数进行改进。Cook和DeRose提出了另一种表示方法,称为小波噪声,它避免了锯齿问题,只增加了很小的评估成本。Liu等使用多种噪声函数来模拟不同的木材纹理和表面光泽。我们还推荐Lagae等人关于这一主题的最新报告。
其他的程序方法是可能的。例如,通过测量从每个位置到一组“散布在空间中的”特征点的距离,就形成了一个细胞纹理。以不同的方式映射结果的最近距离,例如,改变颜色或着色法线,创建看起来像细胞、石板、蜥蜴皮肤和其他自然纹理的模式。Griffths讨论了如何高效地找到最近的邻居,并在GPU上生成细胞纹理。
另一种类型的程序纹理是物理模拟的结果,或者是一些其他交互过程的结果,例如水波或蔓延的裂缝。在这种情况下,程序纹理可以有效地在动态条件下产生无限的变化。
当生成程序二维纹理时,参数化问题可能比创作纹理更加困难,其中可能产生拉伸或接缝伪影问题。一种解决方案是通过直接在表面合成纹理来完全避免参数化。在复杂表面上进行这种操作在技术上具有挑战性,是一个活跃的研究领域。请参见Wei等人对于这个领域的概述。
反走样程序纹理比反走样图像纹理既困难又容易。一方面,像mipmapping这样的预计算方法是不可用的,这给程序员带来了负担。另一方面,程序纹理作者拥有“关于纹理内容的”内部信息,因此可以对其进行调整以避免走样。这对于通过合并多个噪声函数创建的程序纹理尤其适用。每个噪声函数的频率都是已知的,因此任何可能导致走样的频率都可以被丢弃,这实际上降低了计算的成本。有各种各样的技术来反走样其他类型的程序纹理。Dorn等讨论了之前的工作,并提出了一些重新构造纹理函数以避免高频的过程,即限制带宽。
4纹理动画
应用于表面的图像不一定是静态的。例如,视频源可以用作帧与帧之间变化的纹理。
纹理坐标也不需要是静态的。应用程序设计可以显式地在帧与帧之间更改纹理坐标,或者在网格的数据本身中更改,或者通过在顶点着色器或像素着色器中应用的函数更改。比如,一个建完模的瀑布,它通过图像纹理看起来在落水了。其中v坐标是流体的方向。要使水运动,必须从每一帧的v坐标中减去一个量。从纹理坐标中减去一个值有使纹理本身看起来向前移动的效果。
通过将矩阵应用到纹理坐标,可以创建更精细的效果。除了平移,还允许线性变换,如缩放、旋转和剪切、图像扭曲和变形转换以及广义投影。通过在CPU或着色器中应用函数,可以创建许多更精细的效果。
通过使用纹理混合技术,可以实现其他动画效果。例如,从大理石纹理开始,在肉色质感中褪色,就可以使雕像栩栩如生。
5材质贴图
纹理的一个常见用途是修改材质属性,建立着色方程。现实世界中的物体通常具有不同表面的物质属性。为了模拟这样的对象,像素着色器可以从纹理中读取值,并在计算着色方程之前使用它们修改材质参数。最常被纹理修饰的参数是表面颜色。这种纹理被称为albedo color map或diffuse color map。但是,任何参数都可以通过纹理修改:替换它、乘以它或以其他方式更改它。例如,在图6.25中,三个不同的纹理被应用到一个表面,替换了常量值。
原书图6.25,金属工作流的砖和水泥。右边是表面颜色贴图,粗糙度贴图(越亮越粗糙),凹凸高度贴图(越亮越高)。
纹理在材料中的使用可以更进一步。纹理可以用来控制像素着色器本身的流程和功能,而不是修改方程中的参数。两个或多个材质具有不同的着色方程和参数,可以通过使用一个纹理指定表面的哪些区域具有哪些材质来应用于一个表面(可以理解为遮罩),从而为每个材质执行不同的代码。例如,有一些生锈区域的金属表面可以使用纹理来指示生锈的位置,根据纹理查找有条件地执行着色器生锈的部分,否则执行闪亮的金属着色器(第9.5.2节)。
着色模型的输入,如表面颜色,与着色器输出的颜色有线性关系。因此,包含这些输入的纹理可以用标准技术过滤,避免了走样。包含非线性着色输入的纹理,如粗糙度或凹凸贴图(第6.7节),需要更多的注意来避免走样。着色方程的滤波技术可以提高这些纹理的效果。这些技术将在第9.13节中讨论。
6透明贴图
alpha值可以用于许多使用alpha混合或alpha测试的效果,例如快速渲染树叶、爆炸和远处的物体等等。本节将讨论alphas中纹理的使用,并指出各种限制和解决方案。
一个与纹理相关的方面是贴花(decaling)。举例来说,假设希望在茶壶上放一幅花的照片,你不想要整个图片,而只是想要有花的部分。通过为texel分配一个alpha值(0),可以使它透明,这样就不会产生任何效果。因此,通过正确设置贴花纹理的alpha值,可以用贴花替换或混合底层表面。通常,clamp函数与透明界限一起使用,将贴花的单个副本(相对于重复纹理)应用于表面。图6.26显示了如何实现的示例。有关贴花的更多信息,请参见第20.2节。
原书图6.26,实现贴花的一种方法。首先用一个场景渲染帧缓冲区,然后渲染一个框,对于框内的所有点,贴花纹理被投影到帧缓冲区内容中。最左边的texel是完全透明的,因此它不影响帧缓冲区。黄色的texel是不可见的,因为它将被投影到表面的隐藏部分。
alpha的另一个类似应用是制作裁剪。假设你制作了一个贴花的灌木图像,并将它应用到场景中的一个矩形上。其原理与贴花相同,不同之处在于,它不会投影到底面齐平,而是直接在其背后任何几何图形上绘制。通过这种方式,使用单个矩形,可以呈现具有复杂轮廓的对象。
以灌木为例,如果你绕着它旋转场景,这个错觉就会失效,因为灌木没有厚度。一种方法是复制这个矩形并沿树干旋转90度。这两个矩形构成了一个便宜的三维灌木,有时被称为交叉树。从地面上看,这种错觉相当有效,如图6.27。Pelzer讨论了使用三个裁剪来表示草的类似构造。在第13.6节中,我们将讨论一种称为billboarding的方法,它用于将这种渲染减少到单个矩形。如果视角移动到上面,这种错觉就会消失,因为从上面可以看到灌木是两个裁剪的。参见图6.28。为了解决这个问题,可以以不同的方式添加更多的裁剪——切片、分支、图层——以提供更令人信服的模型。第13.6.5节讨论了生成此类模型的一种方法;图19.31显示了另一个。有关最终结果的示例,请参见原书第2页和第1049页上的图像。
原书图6.27,左边是灌木纹理贴图和1位alpha通道贴图。右边是渲染在单个矩形上的灌木;通过添加旋转90度的矩形第二个副本,形成了一个廉价的三维灌木。
原书图6.28,从离地面稍远一点的地方看“交叉树”灌木,然后再往上看,错觉就消失了。
结合alpha图和纹理动画可以产生让人信服的特殊效果,如喷枪、植物生长、爆炸和大气效果。
有几个选项可以用来渲染带有alpha贴图的对象。 alpha blending(章节5.5)允许部分透明,这允许抗锯齿的边缘,以及部分透明对象。然而alpha blending要求要在不透明物体之后渲染,并按前后顺序排列渲染。不过交叉树使用的是两个裁剪纹理,每个四边形互相交叉,没有正确的渲染顺序。即使在理论上可以排序并得到正确的顺序,但这样做通常是低效的。例如,一个坡上可能有成千上万的草,每个网格对象有许多单独的插片。对每个插片进行排序是非常不切实际的。
这个问题在渲染时可以通过几种不同的方法来改善。一种是使用alpha testing,这是一个根据条件丢弃alpha值低于像素着色器中给定阈值的片段的过程。
texture.a是来自纹理查找的alpha值,alphaThreshold是用户提供的阈值,它决定哪些片元将被丢弃。这个方法允许三角形以任意顺序渲染,因为透明片元会被丢弃。通常对alpha为0.0的片元这样做。额外好处是可以进一步节省着色器的处理和合并成本,以及避免在z-buffer中错误地将像素标记为可见的。对于裁剪,我们通常将阈值设置为比0.0高,0.5或更高,然后进一步忽略alpha值,不使用它进行混合。这样做可以避免渲染顺序错误问题。但是这样渲染质量比较低,因为只有完全透明和完全不透明。另一种解决方案是对每个模型执行两次pass。一个用于实体裁剪,被写入z-buffer。另一个用于半透样本,不写入z-buffer。
alpha 测试还有另外两个问题,即放大太多和缩小太多。当alpha测试与mipmapping一起使用时,如果处理不当,结果可能不正确。图6.29上面显示了一个示例,其中树的叶子变得比预期更加透明。可以用一个例子来解释。假设我们有一个具有四个alpha值的一维纹理(0.0,1.0,1.0,0.0)。平均后下一个mipmap级别变为(0.5,0.5),然后最后是(0.5)。现在假设$\alpha_t = 0.75$,当访问mipmap级别为0时,可以看到4个纹素中有一半纹素可以通过测试。但是访问下两个级别时,所有内容都将被丢弃,因为0.5小于0.75.另一个示例参见图6.30。
原书图6.29,上面是alpha test与mipmapping没有修正前。下面是根据覆盖率重新调整alpha值后。
原书图6.30,在顶部是使用alph blending的不同mipmap层次的叶片,逐步放大后的显示。在底部显示的是以0.5为alpha testing阈值的不同mipmap层次的叶片,显示了像素是如何减少的。
Castano提供了一个简单的解决方案,在创建mipmap时使用。对于mipmap等级k,覆盖率$c_k$定义为
其中$n_k$是mipmap等级k中的texel数,$\alpha(k,i)$是mipmap等级k在像素i处的alpha值,$\alpha_t$为公式6.9中用户提供的alpha阈值。我们假设$\alpha(k,i)>\alpha_t$如果为真,结果为1,否则为0。注意,k = 0表示mipmap的最低级别即原图。对于每个mipmap级别,我们找到一个新的mipmap阈值$\alpha_k$代替$\alpha_t$。这样$c_k$就会无限接近于$c_0$。这可以通过二分查找来实现。最后在mipmap级别k中所有texel的alpha值都按$\alpha_t/\alpha_k$进行缩放。图6.29就使用了这种方法,NVIDIA的纹理工具支持这种方法。Golus给出了一个方法变体,不修改mipmap,但是alpha值在着色器中随着mipmap级别的增加而增加。
Wyman和McGuire[1933]提出了一个不同的解决方案,其中公式6.9中的代码行在理论上被替换为 :
随机函数范围是[0,1],这意味着平均而言有正确的结果。 例如,如果纹理查找的alpha值是0.3,那么片段将有30%的几率被丢弃。这是一种随机透明的形式,每个像素只有一个样本。 在实际应用中,为了避免时空高频噪声,将随机函数替换为哈希函数:
三维散列是通过对上述函数的嵌套调用形成的,即float hash3D(x,y,z) { return hash2D(hash2D(x,y),z); },它返回一个值[0,1)。 散列的输入是对象空间坐标除以对象空间坐标的最大屏幕空间导数(x和y),然后是clamp。为了获得z方向运动的稳定性,需要进一步的注意,并且该方法最好与时间抗锯齿技术相结合。这种技术随着距离的增加而逐渐消失,所以在近距离观察时,我们根本不会得到任何随机的结果。这种方法的优点是平均每个片段都是正确的,而Castano的方法为每个mipmap级别创建一个$\alpha_k$。然而,这个值可能在每个mipmap级别上有所不同,这可能会降低质量并需要美术干预。
Alpha测试放大显示下的锯齿,可以通过将Alpha映射作为距离字段预先计算来避免(参见第677页的讨论)。
Alpha to coverage(覆盖率),以及类似的特性transparency adaptive antialiasing,获取片段的透明度值,并将其转换为一个像素内包含多少个样本 。这个想法就像第5.5节中描述的屏幕门透明度,但在亚像素级别。 假设每个像素有四个样本位置,一个片段覆盖一个像素,但是由于裁剪纹理,它有25%是透明的(75%是不透明的)。 Alpha to coverage模式使片段变得完全不透明,但是它只覆盖了四个样本中的三个。这种模式对于切割重叠的草叶纹理非常有用。由于绘制的每个样本都是完全不透明的,所以最靠近的叶片会沿着边缘以一致的方式隐藏后面的物体。正确地混合半透明的边缘像素不需要排序,因为alpha混合关掉了。
Alpha to coverage对于抗锯齿Alpha测试来说是很好的,但是在Alpha混合的时候会显示伪像。例如,两个具有相同alpha覆盖率的alpha混合片段将使用相同的亚像素模式,这意味着一个片段将完全覆盖另一个片段,而不是与它混合。Golus讨论了使用fwidth()着色器指令来赋予内容更清晰的边缘,见图6.31 。
原书图6.31,不同渲染技术的叶片纹理与部分alpha覆盖的边缘。从左到右:alpha test,alpha blend,alpha to coverage,alpha to coverage锐化边缘。
对于任何使用的alpha映射,重要的是要了解双线性插值如何建立颜色值。想象两个相邻的texel: rgb = (255,0,0,255)是一个纯红色,它的邻居rgba = (0,0,0,2),是几乎完全透明的黑色。位于两个texel之间的位置的rgb是什么?简单插值给出(127,0,0,128)。结果的rgb值是一个“红色淡光”。然而,这个结果实际上并不是更暗的,它是一个完全的红色,只是已经预先乘以它的alpha。如果你插值alpha值,为了得到正确的插值,你需要确保被插值的颜色在插值之前已经被预先乘以alpha了。例如,假设将几乎透明的邻居设置为rgb = (0,255,0,2),给人一种极细微的绿色。这个颜色没有预先乘以alpha,当插入时结果是(127,127,0,128),微小的绿色色调突然将结果变成一个(预乘的)黄色样本。这个相邻的texel的预乘版本是(0,2,0,2),从而得到(127,1,0,128)。这个结果更有意义,结果的预乘颜色大多是红色,带有一点难以察觉的绿色。
忽略双线性插值会导致在贴花和裁剪对象周围产生黑边。黯淡的红色结果被管线的其余部分作为未乘色处理,边缘变为黑色。即使使用alpha测试,也可以看到这种效果。最好的策略是在双线性插值之前进行预乘。WebGL API支持这一点,因为合成对于网页来说很重要。然而,双线性插值通常是由GPU执行的,着色器无法在对texel值进行操作之前完成该操作。图像在PNG等文件格式中没有预乘,因为这样做会失去颜色精度。当使用alpha映射时,这两个因素结合在一起会导致默认的黑色边缘。一种常见的解决方法是对裁剪图像进行预处理,将透明的黑色“texel”涂上一种来自附近不透明texel的颜色 。所有透明区域经常需要以这种方式重新绘制,手工或自动绘制,以便mipmap级别也避免边缘问题。同样值得注意的是,在使用alpha值形成mipmaps时,应该使用预乘值。
7凹凸贴图
本节描述了一大类我们统称为凹凸贴图(Bump mapping)的小规模细节表现技术。所有这些方法通常都是通过修改每个像素的着色过程来实现的。它们比单独的纹理映射提供了更三维的外观,但是没有添加任何额外的几何图形。
一个物体的细节可以分为三种尺度:覆盖多个像素的宏观特征、跨越几个像素的中观特征和实质上小于一个像素的微观特征。这些类别在某种程度上是变化的,因为在动画或交互期间,观察者可以在许多距离上观察同一个对象。
宏观几何由顶点和三角形或其他几何图元表示。创建三维角色时,四肢和头通常是在宏观尺度上建模的。微几何封装在阴影着色中,通常在像素着色器中实现,并使用纹理作为参数。使用的着色模型模拟了一个表面的微观几何结构的相互作用,例如,有光泽的物体在微观下是光滑的,而漫反射的表面在微观下是粗糙的。角色的皮肤和衣服表现不同,因为它们使用不同的着色器,或者至少在那些着色器中有不同的参数。
中观几何描述了这两个尺度之间的一切。它包含的复杂细节无法有效使用单独的三角形进行渲染,但也足以让观察者分辨出表面的个别变化。人物脸上的皱纹,肌肉组织的细节,衣服的褶皱和接缝,都是中尺度的。一系列统称为凹凸映射(bump mapping)技术的方法通常用于中尺度建模。它们在像素级调整底纹参数,这样观众就能感知到基本几何形状的微小扰动,而基本几何形状实际上是平坦的。不同凹凸贴图类型之间的主要区别是它们如何表示细节特征。变量包括写实程度和细节特征的复杂性。例如,数字艺术家通常将细节雕刻到模型中,然后使用软件将这些几何元素转换成一个或多个纹理,例如bump纹理和crevice-darkening纹理。
Blinn在1978年提出了在纹理中编码中尺度细节的想法。他观察到如果在渲染时我们用一个轻微扰动的表面正常代替真正的表面,会产生一些小细节。他将描述扰动的数据存储到表面法线中。
关键思想是,不是使用纹理来改变光照方程中的颜色分量,而是使用纹理来修改表面法线。曲面几何法向保持不变,只是修改了在光照方程中使用的法线。这个操作不是物理上的等价,我们改变表面的法线,但表面本身在几何意义上保持光滑。就像每个顶点都有一个法向量会给人一种表面在三角形之间是光滑的错觉,修改每个像素的法向量会改变三角形表面本身的感知,而不需要修改它的几何形状。
对于凹凸贴图,法线必须相对于某些参照系改变方向。为此,在每个顶点上存储一个切线坐标系(tangent frame),也称为切线空间基(tangent-space basis)。这个参照系被用来将光转换成一个表面位置的空间(或者相反)来计算扰动法向的影响。对于应用了法线贴图的多边形曲面,除了顶点法线外,我们还存储了切线向量和双切向量。双切线向量(bitangent vector)也被错误地称为副法线向量(binormal vector)。
正切和双切向量表示法线贴图本身在物体空间中的轴,目标是将光线转换成相对于贴图的光线。参考图6.32。
原书图6.32,一个球面三角形其切线框架显示。像球体和圆环面的形状有一个自然的切线空间基础,就像圆环面上的经纬度线所显示的那样。
这三个向量,法向量n、切线t和双切线b,构成了一个基矩阵:
这个矩阵,有时缩写为TBN,将光的方向(对于给定的顶点)从世界空间转换为切线空间。这些向量不必相互垂直,因为法线映射本身可能会被扭曲以适应曲面。然而,非正交基会导致纹理倾斜,这意味着需要更多的存储空间,也会影响性能,即这个矩阵不能被一个简单的转置倒转。一种节省内存的方法是只存储顶点处的tangent和bitangent,然后取它们的叉乘来计算法线。然而,这种技术只有在矩阵的左右手倾向性总是相同的情况下才有效。模型通常是对称的如飞机、人等。因为纹理消耗大量的内存,所以它们经常被镜像到对称模型上。因此,只存储对象纹理的一侧,但是纹理映射将其放在模型的两侧。在这种情况下,切线空间的左右手倾向性在两边是不同的,不能假设。在这种情况下,如果在每个顶点上都存储了额外的信息以表明惯用手,那么仍然可以避免存储法线。如果设置,这个位是用来使叉乘的切线和双切线产生正确的法线。如果正切坐标系是正交的,也可以将基存储为四元数,更节省空间并且可以节省每像素的计算量。质量上的小损失是可能的,尽管在实践中很少见到。
切线空间的概念对其它算法也很重要。正如在下一章所讨论的,许多着色方程仅依赖于表面的法线方向。然而如拉丝铝或天鹅绒材质也需要知道观察者和灯光相对于表面的方向。切线坐标系对于定义材质在表面上的方向很有用。Lengyel和Mittring的文章对这一领域进行了广泛的论述。Schuler提出了一种在像素着色器中动态计算切线空间基的方法,不需要为每个顶点存储预先计算好的切线基。Mikkelsen对这项技术进行了改进,并得出了不需要任何参数化的方法,而是使用表面位置的导数和高度场的导数来计算扰动的法线。但是与使用标准的切线空间映射相比,这种技术所显示的细节要少得多,并且可能会产生艺术工作流问题。
7.1Blinn的方法
Blinn的原始凹凸映射方法在纹理的每个texel上存储两个符号值$b_u$和$b_v$。这两个值对应的是沿着u和v图像轴改变法线的量。也就是说,这些纹理值,通常是双线性内插的,用来缩放两个垂直于法线的向量。这两个向量加到法向量上改变方向。这两个值描述了曲面在该点的方向。参见图6.33。这种类型的凹凸贴图纹理被称为偏移向量凹凸贴图(offset vector bump map)或偏移贴图(offset map)。
另一种方法是使用高度场来修改表面法线。每个单色纹理值表示一个高度,因此在纹理中,白色表示高区域,黑色表示低区域(反之亦然)。示例参加图6.34。
原书图6.33,左边是一个法向量n使用bump贴图上的$b_u$和$b_v$计算得到n’(未归一化)。右边显示了高度场及其对着色法线的影响。这些法线可以插在高度之间以使外观更平滑。这是第一次创建或扫描凹凸贴图时使用的常见格式,也是Blinn在1978年引入的格式。高度场用于导出u和v有符号的值,类似于第一种方法中使用的值。这是通过获取相邻列之间的差异来获得u的斜率,以及获取相邻行之间的差异来得到v的斜率实现的。v的一种变体是使用Sobel滤波器,该滤波器对直接相邻的邻居赋予更大的权重。
原书图6.34,在一个球体上使用波纹高度凹凸映射。
7.2法线映射
凹凸贴图的一个常用方法是直接存储一个法线贴图。算法和结果在数学上与Blinn方法一致;只有存储格式和像素着色器计算改变。
法线贴图编码(x,y,z)到[-1,1]。对于一个8位的纹理,0代表了-1,255代表了1.如图6.35,浅蓝色[128,128,255]将代表一个平面的颜色映射,即法线[0,0,1]。
原图6.35,使用法线贴图映射凹凸。每个颜色通道实际上是一个表面法线坐标。红色通道是x轴偏差;越红,法线越向右。绿色是y的偏移,蓝色是z,右边是使用法线贴图生成的图像。注意立方体顶部的扁平外观。
法线贴图最初是作为世界空间法线图引入的,但在实践中很少使用。对于这种类型的映射,扰动是直接的:在每个像素处,从映射中检索法线并直接使用它,连同光的方向,来计算该位置在表面上的着色。还可以在对象空间中定义法线贴图,以便可以旋转模型后法线仍然有效。 但是,世界和对象空间表示都将纹理绑定到特定方向的特定几何体,这限制了纹理的重复使用。
相反,扰动的法线通常在切线空间中检索,即相对于表面本身。这允许表面的变形,以及最大限度地重复使用法线纹理。切线空间法线也可以压缩得很好,因为z分量的符号(与未扰动曲面法线对齐的那个)通常可以假定为正的。
法向映射可以用来很好地提高显示的真实性,见图6.36。
原书图6.36,一个游戏场景中使用的法线贴图映射凹凸的例子。左上:右边的两个法线不应用。左下:应用法线贴图。右图:法线贴图。
与过滤彩色纹理相比,过滤法线贴图是一个比较困难的问题。一般来说,法线和着色之间的关系不是线性的,所以标准的滤波方法可能会导致令人讨厌的伪像。想象一下,楼梯是由闪闪发光的白色大理石砌成的。在某些角度,楼梯的顶部或侧面可以捕捉到光线并反射出明亮的高光。然而,楼梯的法线角度是45度;它将从比原来的楼梯完全不同的方向捕捉亮点。如果在没有正确过滤的情况下渲染具有尖锐的镜面高光的凹凸贴图,则由于高光采样位置而闪烁,会产生分散注意力的闪光效果。
Lambertian朗伯曲面是一种特殊的情况,它的法线贴图在着色上几乎是线性的。Lambertian着色几乎完全是一个点积,它是一个线性操作。对一组法线求平均值并对结果执行点积,相当于对单个点积与法线求平均值:
注意,平均向量在使用之前没有标准化。公式6.14结果表明,标准填充法和mipmaps法对Lambertian曲面的填充效果基本一致。结果不太正确,因为Lambertian着色方程不是一个点乘,是一个限制点乘——max(l·n,0)。clamp操作让它非线性。这将使表面过度变暗,以使光的方向发生偏光,但在实践中这通常是不受反对的。一个警告是,通常用于法线贴图的某些纹理压缩方法(例如从其他两个法线重构z分量)不支持非单位长度的法线,因此使用非标准化的法线贴图可能会带来压缩困难。
在非Lambertian曲面的情况下,通过作为一个整体过滤着色方程的输入来产生更好的结果是可能的,而不是孤立地过滤法线贴图。这样做的技术将在第9.13节中讨论。
最后,从高度图h(x,y)导出法线图可能是有用的。首先,在x和y方向上的导数的近似值是用中心差分来计算的:
在texel处的未归一化法线(x,y)为:
必须注意纹理的边界。通过使凹凸能够将阴影投射到其自身的表面上,可以将水平贴图用于进一步增强法线贴图。通过预先计算其他纹理(每个纹理与沿着曲面平面的方向相关联)并为每个纹理像素存储该方向上的水平角度,可以完成此操作。有关更多信息,请参见第11.4节。
8视差贴图
凹凸和法线贴图的一个问题是凹凸不会随着视角改变位置,也不会互相遮挡。例如,如果你沿着真正的砖墙看,在某个角度你不会看到砖与砖之间的灰浆。墙壁的凹凸贴图永远不会显示这种类型的遮挡,因为它只是改变了法线情况。最好是让凹凸实际上是在每个像素的表面位置上渲染。
视差贴图的概念是由Kaneko在2001年提出的,并由Welsh改进和推广。视差是指当观察者移动时,物体的位置相对移动。当观众移动时,凹凸应该看起来有高度。视差映射的关键思想是通过检查可视像素的高度来对像素中应该看到的内容进行有根据的猜测。
对于视差映射,凹凸信息存储在高度场贴图中。给定像素处查看表面时,在该位置检索高度值,并将其用于移动纹理坐标以检索表面的不同部分。移动的幅度取决于重置高度和眼睛到表面的角度。参见图6.37。高度值存储在一个单独的纹理贴图或包含在未使用的颜色或其他贴图的alpha通道中(在打包不相关的纹理时必须小心,因为这会对压缩质量产生负面影响 )。在使用坐标移动之前,高度值会进行缩放和偏移。尺度scale决定了高度场在地表之上或之下延伸的高度,而偏差bias给出了海平面高度,在这个高度上不会发生位移。给定纹理坐标位置p,调整后的高度场高度h和具有高度值$v_z$和水平分量$v_{xy}$的归一化视图矢量v,则视差调整后的纹理坐标$P_{adj}$为:
请注意,与大多数着色方程不同,此处执行计算的空间与视图向量需要在切线空间。
原书图6.37,左侧是目标:从视图向量发射到高度场的位置找到表面上的实际位置。视差贴图通过获取矩形上某个位置的高度并使用它来找到新的位置$P_{adj}$来进行近似。
虽然这只是一个简单的近似,但在实际应用中,如果凹凸高度变化相对缓慢,这种变化就会相当有效。邻近的texel也有相同的高度,所以使用原始位置的高度来估计新位置的高度是合理的。然而,这种方法在浅视角下就行不通了。当视场向量接近地表水平线时,一个小的高度变化会导致大的纹理坐标偏移。由于所检索到的新位置与原始表面位置的高度相关性很小或没有相关性,因此这种近似是失败的。
为了改善这个问题,Welsh引入了偏移限制的思想。这样做的目的是限制移动量,使其永远不会超过检索到的高度。方程是:
请注意,该公式的计算速度比原始公式要快。从几何学上这种解释是,高度定义了一个半径,位置不能超出该半径。如图6.38所示。
原书图6.38,在视差偏移限制中,偏移量最多移动距离原始位置的高度,显示为虚线圆弧。灰色的是原始结果,黑色的是有限结果。右边是用这种技术绘制的墙。
在陡(面)角处,由于$v_z$接近1,这个方程几乎与原来的方程相同。在较浅的角度,偏移量在其影响范围内变得有限。在视觉上,这使得凹凸性在浅角度上减少,但这比随机采样纹理要好得多。当视图改变时,纹理也会出现问题,或者在立体渲染中,当观察者同时感知两个视点时,必须给出一致的深度提示。即使有这些缺点,视差映射与偏移t限制成本只是几个额外的像素着色器程序指令,并提供了一个相当大的图像质量改善比基本的法线映射。Shishkovtsov通过向凹凸贴图法线方向移动估计位置来改善视差遮挡的阴影。
8.1视差遮蔽贴图
凹凸贴图不修改基于高度场的纹理坐标;它只改变一个位置的着色法线。视差映射提供了一个简单的高度场效应的近似,假设一个像素点的高度与其相邻像素点的高度大致相同。这种假设很快就会被打破。凹凸不会相互遮挡,也不会产生阴影。我们想要的是在像素处可见的东西,即视图向量首先与高度场相交的地方。
为了更好地解决这个问题,一些研究人员建议使用射线沿着视图向量行进,直到找到一个(近似的)交点。这项工作可以在像素着色器中完成,其中高度数据可以作为纹理访问。我们将对这些方法的研究归纳为视差映射技术的一个子集,这些技术用不同的方式利用射线。
这些类型的算法被称为视差遮挡映射(parallax occlusion mapp,POM)或浮雕映射方法(relief mapping)。关键的思想是先沿着投影向量测试固定数量的高度场纹理样本。对于掠射角度的视线射线,通常会生成更多的样本,这样就不会错过最近的交点。检索沿射线的每个三维位置,将其转换为纹理空间,并进行处理以确定其是否在高度场之上或之下。一旦找到了高度值以下的样本,则使用其下的数量以及上一个样本上的数量来查找相交位置,见图6.39。然后使用该位置使用附加的法线贴图,颜色贴图和任何其他纹理对表面着色。 多层高度场可用于产生悬崖,独立的重叠表面以及双面伪浮雕; 请参阅第13.7节。高度场跟踪方法还可用于使凹凸的表面在其自身上投射阴影,包括硬阴影和软阴影。 比较请参见图6.40。
原书图6.39,绿色视线射线投射到表面上,每隔一定的时间采样(紫色的点),然后获取高度。该算法求出了人眼射线与拟合曲线高度的黑色线段的第一个交点。
原书图6.40,没有射线行进的视差贴图(左)与有射线行进的视差贴图(右)。当射线行进不使用时立方体的顶部有衰减。随着射线行进,也产生了自阴影效果。
关于这个话题有大量的文献。虽然所有这些方法都是使用射线前进的,但是有几个不同之处。可以使用简单的纹理来检索高度,但也可以使用更高级的数据结构和更高级的根查找方法。一些技术可能涉及到着色器丢弃像素或写入深度缓冲,这可能会影响性能。下面我们总结了大量的方法,但是请记住,随着gpu的发展,最好的方法也在发展。这个“最佳”的方法取决于光线行进过程中的内容和步数。
确定两个常规样本之间的实际交点是一个求根问题。实际上,高度场更多地被视为深度场,矩形的平面定义了表面的上限。这样,平面上的初始点在高度场上方。在找到高度场表面上的最后一个点和下面的第一个点之后,Tatarchuk使用割线法的单个步骤来找到近似解。Policarpo等人利用二叉搜索法在两个点之间找到一个更接近的交叉点。Risser等人通过使用割线法迭代来加速收敛。tradeo的优点是可以并行地进行常规采样,而迭代方法需要较少的总体纹理访问,但必须等待结果并执行较慢的依赖纹理获取。蛮力方法似乎总体表现良好。
足够频繁地采样高度场至关重要。McGuire建议对mipmap查找进行偏倚,并使用各向异性的mipmap来确保对高频高度场(如表示峰值或毛发的高度场)进行正确的采样。你也可以存储比法线贴图分辨率更高的高度场纹理。最后,一些渲染系统甚至不存储法线贴图,而更倾向于使用一个交叉过滤器从高度场动态导出法线。公式16.1给出了方法。
提高性能和采样精度的另一种方法是不首先对高度场进行定期采样,而是尝试跳过中间的空白空间。Donnelly将高度场预处理成一组体素,并在每个体素中存储它与高度场表面的距离。通过这种方式,可以快速跳过中间的空间,以每个高度场更高的存储空间为代价。Wang等人使用五维位移映射方案来保持从所有方向和位置到表面的距离。这允许复杂的曲面、自阴影和其他效果,代价是相当大的内存。Mehra和Kumar出于类似的目的使用方向距离贴图。Dummer引入了锥阶映射的思想,并且Policarpo和Oliveira对其进行了改进。这里的概念是为每个高度场位置存储一个圆锥体半径。这个半径定义了与高度场最多有一个交点的射线的一个区间。这个属性允许沿着光线快速跳跃而不丢失任何可能的交叉点,尽管代价是需要依赖的纹理读取。另一个缺点是创建圆锥步长映射所需的预计算,这使得该方法不能用于动态更改高度场。Schroders和Gulik提出了四叉树地形映射,这是一种在遍历过程中跳过体积的分层方法。Tevs等人使用“最大mipmaps”来允许跳过,同时最小化预计算成本。Drobot还使用存储在mipmaps中的类似四叉树的结构来加速遍历,并提供了一种混合不同高度场的方法,其中一种是地形类型转换为另一种地形类型。
上述所有方法的一个问题是,沿着物体轮廓边缘的错觉会被打破,这将显示出原始表面的光滑轮廓。参见图6.41。关键的思想是,渲染的三角形定义了像素着色器程序应该评估哪些像素,而不是表面的实际位置。此外,对于曲面,轮廓的问题变得更加复杂。Oliveira和Policarpo描述和开发了一种方法,使用二次剪影近似技术。Jeschke等人和Dachsbacher等人都给出了一种更通用和健壮的方法(并回顾了以前的工作)来正确处理轮廓和曲面。Hirche首先提出的基本思想是将网格中的每个三角形向外挤压,形成棱镜。渲染这个棱镜时,会对所有可能出现高度场的像素进行评估。这种方法称为shell映射,因为扩展网格在原始模型上形成一个单独的shell。通过在与光线相交时保留棱镜的非线性特性,可以实现对高度场的无伪影渲染,尽管计算起来很昂贵。这种技术的一个令人印象深刻的使用如图6.42所示。
原书图6.41,法线映射和浮雕映射。法线映射不会发生自遮挡。对于重复的纹理,浮雕映射在轮廓上有问题,因为矩形更多的是进入高度场的视角,而不是真正的边界定义。
原书图6.42,视差遮挡贴图,又称浮雕贴图,用于使石头看起来更真实的路径。地面实际上是一组简单的三角形,应用了高度场。
9纹理光照
纹理也可以用来增加光源的视觉丰富性,并允许复杂的强度分布或聚光灯功能。对于所有照明都限制在圆锥或截锥体上的光,可以使用投射纹理来调节光的强度。这允许形状的聚光灯,有图案的灯,甚至幻灯片的效果(图6.43)。这些灯通常被称为gobo或cookie灯,这是根据专业剧院和电影照明中使用的术语来命名的。有关类似方式投射阴影的讨论,请参见第7.2节。
原书图6.43,投影变形。纹理被投射到茶壶和地平面上,用来调节光在投射截锥体内的贡献(在截锥体外设置为0)。
对于不局限于截锥体,而是向各个方向照射的光线,可以使用一个立方体映射来调节强度,而不是二维投影纹理。一维纹理可以用来定义任意距离衰减函数。结合一个二维的角度衰减图,这可以实现复杂的体积照明模式。一个更普遍的可能性是使用三维(体积)纹理来控制光线的衰减。这允许任意体积的效果,包括光束。这种技术是内存密集型的(就像所有的体积纹理一样)。如果光的体积效应沿三个轴对称,通过将数据镜像到每个八分区,内存占用可以减少八倍。
纹理可以添加到任何光类型,以启用额外的视觉效果。带纹理的灯光使艺术家可以轻松控制照明,他们可以简单地编辑所使用的纹理。
扩展阅读和参考资料
具体文献见原书引用资料
推荐网站:Shadertoy