"All it takes is for the rendered image to look right."—Jim Blinn。翻译过来就是,所做的一切,都是为了使得渲染出来的图片看起来更加真实。(Patrick:Jim Blinn,就是提出了Blinn-Phong模型的图形学界大牛。Blinn-Phong模型与Phong模型的区别是,把dot(V,R)换成了dot(N,H),其中H为半角向量,位于法线N和光线L的角平分线方向。)

表面的纹理是它的外观和感觉——想象油画的纹理就知道了。在计算机图形学中,纹理是用于生成表面和在每个位置通过一些图片、函数以及其它数据修改表面的过程。举个例子,与其创建一个完全精确的砖墙几何体,不如用一张砖墙的照片,贴到一个由两个三角形组成的方形上。当观看这个方形的时候,就可以看到方形上贴的这个图片。除非观察者走的非常近去观看,否则不会注意到这个图片缺乏了一些几何细节。

然而,除了缺乏几何细节之外,还可能会有一些其它问题。比如,砖墙上的石灰应该是没有高光的,而砖墙上的砖头应该是有点光泽的,如果只有一张颜色贴图,观察者会注意到,这两种材质的粗糙度是相同的。为了产生更令人信服的体验,需要对物体表面使用第二张纹理贴图。这张贴图不会影响表面颜色,而是会针对砖墙表面上的任意位置,改变其粗糙度。这样的话,砖墙上的砖头和石灰具有来自图像纹理的颜色以及这张新纹理的粗糙度值。

观察者可以看到,现在所有砖头都是光滑的,而石灰不是的。但是仔细看每个砖头的表面,看起来都是完全平坦的。这一点不太对,因为砖头的表面理论上应该是不规则的。可以通过一张bump map,通过修改砖头的着色法线,以便在渲染的时候,使其看起来并不完全平滑。这种纹理是用于,在计算光照的时候,调整矩形原始表面的法线使用的。

从一个近似0角度的视角观看的话,bump map所产生的这种不平的感觉又会消失了。理论上砖头应该会凸出于石灰,从观察的角度来看会挡住这些石灰。如果从垂直角度观看的话,砖头应该能在石灰上投下阴影。Parallax map视差贴图就是使用纹理在渲染平面的时候,使其看起来变形,而parallax occlusion map则是将光线投射到heightfield纹理,以提高真实感。Displacement map通过修改模型的三角形高度来真实的改变了物体表面。图6.1是一个使用了颜色纹理和bump纹理的例子。

Texture

上图中可以看到,将颜色贴图和bump贴图用于这条鱼,可以增加其视觉细节水平(图片由Elinor Quittner提供)

上述为通过纹理解决各种各样问题的例子,相关联的使用到了越来越精细的算法。在本章中,将详细介绍纹理技术。首先,提出了处理纹理的整个框架。然后,我们会重点关注用于物体表面的纹理,这也是实时渲染中纹理的最常见的一种方式。之后还会简单的聊一下程序纹理,并会聊一些通过纹理影响物体表面的方法。

6.1 纹理管线

纹理可以有效的模拟物体表面的材料和光滑度。研究纹理可以通过研究单个着色像素的情况的方法。如前一章所述,着色是通过将材质的颜色、光照等因素放在一起考虑的。如果有的话,半透明度也会影响采样样本。纹理是通过修改着色公式中使用的值来工作。这些数值的改变是基于物体表面的位置。比如,砖墙这个例子,根据物体表面的位置,使用砖墙图像上的相应位置的颜色来代表。为了和屏幕上的像素区分,纹理图片上的像素通常被称为纹素。粗糙度纹理可被用于修改粗糙度值,法线纹理用于修改着色法线的方向,因此,每个都可以被用于改变着色方程式的结果。

可以用一个纹理管线的方式来描述纹理。下面将会对这其中的相关技术,详细的一一介绍。

空间中的一个位置,是纹理处理的起点。此位置可以位于世界空间中,但更经常位于模型的参考系中,因此随着模型移动,纹理也随之移动。使用Kershaw的术语(参考文献[884]),空间中的这个点应用了一个投影函数,以获得一组数字,被称为纹理坐标,这组数字被用于访问纹理。这个过程称为映射,也就产生了纹理映射texture map。有时,纹理图像本身被称为texture map,尽管这并不完全正确。

在使用这组纹理坐标去访问纹理之前,可以使用一个或者多个对应函数将纹理坐标转换到纹理空间。这些纹理空间位置被用于从纹理获取数值,例如,它们可以是图像纹理的数组索引以检索像素。然后,检索到的值可能会再次被值转换value transform函数转换,最后这些新值被用于修改曲面的某些属性,比如材质或者着色法线。图6.2详细展示了单一纹理的应用过程。管道如此复杂的原因是,每个步骤都为用户提供了一个有用的控件。需要注意的是,并非所有步骤都是必须被执行的。

Texture

上图为单一纹理的纹理管线。

使用这个管线,当三角形具有砖墙纹理,并且在其表面进行采样时,就会发生这种情况(详见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,那么就得到了纹理空间中的坐标(81.92,74.24)。舍去小数点部分,下面就可以在砖墙图像中找到像素(81,74),其颜色为(0.9,0.8,0.7)。假设纹理是sRGB空间,因此如果要在着色方程中使用该颜色,则将其转换为线性空间,得到(0.787,0.604,0.448)(详见5.6节)。

Texture

上图为砖墙的管线。

(Patrick:这里有点复杂,不过还好有个例子,最后一步线性空间的转换,其实也有其它函数,比如normal map的话就是*2-1之类的)

6.1.1 映射函数

纹理处理的第一步是根据表面的位置,并将其映射到纹理坐标空间,通常会是一个二维(u,v)空间。建模工具通常会允许美术人员逐顶点的自定义其(u,v)坐标。uv坐标可以通过映射函数或者mesh展开算法进行初始化。美术同学可以按照编辑顶点位置的方式,编辑(u,v)坐标。映射函数通常是将空间中的一个3D位置,转换为纹理坐标。建模工具中,常用的函数包括球形、圆柱形和平面映射(参考文献[141,884,970])

映射函数还有一些其它方式。比如,通过表面法线可以为表面选择使用六个平面映射中的哪一个。匹配纹理的问题经常会出现在面相交的接缝处,Geiss(参考文献[521,522])介绍了一种用于这种情况的blend技术。Tarini等人(参考文献[1740])介绍了一种polycube的映射技术,将一个模型使用多个cube映射函数,针对不同空间使用不同的cube映射函数。

还有一些映射函数根本不是映射,而是一种隐式的表面创建和tessellation。例如,mesh中定义了一组(u,v)坐标,如图6.4。而纹理坐标也可以通过各种不同的参数生成,比如观察方向,表面温度或者其它任何可以想到的参数。映射函数的目的就是为了生成纹理坐标,通过位置来生成它们只是其中一个方法。

Texture

上图展示了不同的纹理投影。从左到右展示了球形、圆柱形、平面和自然natural(u,v)投影。而下面这一行展示了将不同投影应用在物件上的效果(其中没有natural投影)

非交互式的渲染通常将这些投影函数称为渲染过程中的一部分。可以将一个单一的投影函数应用在整个模型上,然而艺术家通常会使用工具将模型分成若干部分,然后分别应用多个投影函数(参考文献[1345])。如图6.5所示。

Texture

上图展示了如何在一个模型上使用多个投影函数。其中box mapping包含了6个平面mapping,box的每一面都包含一个(图片由Tito Pagan提供)

在实际工作中,投影函数通常是在建模阶段完成,然后将投影结果存储在顶点中。当然,有时候也并非如此,比如有时也会在vs或者ps阶段进行投影,这样做的好处是可以提高精度,并有助于启用各种效果,比如动画(详见6.4节)。还有一些渲染方法,比如环境映射(详见10.4节),有自己的专用投影函数,这些函数是逐像素操作的。

(Patrick:还想不到动画为什么要在vs、ps阶段进行投影,但是环境映射,本质就是去采样环境贴图,确实是根据像素所在的位置,然后生成一个uv值,去采环境贴图。)

球面投影(图6.4节的左侧)将点投射到围绕某个点的虚拟球体上。此投影与Blinn和Newell的环境映射方案(详见10.4.1节)所使用的投影函数一样,第407页的方程式10.30描述了此函数。该投影函数与该节中描述的顶点插值有相同的问题。

(Patrick:虽然还没看到第10章,但是可以想象到,在顶点中做球面投影必然会有精度丢失的问题。。)

圆柱投影和球面投影针对纹理坐标中的u处理方式是一样的,而纹理坐标中的v是沿着圆柱投影轴计算。这种投影函数针对有自然轴的对象(比如旋转曲面)很有用。而当表面几乎垂直于圆柱体轴的时候,就会发生变形。

平面投影类似于x射线束,沿一个方向平行投影,并将纹理应用于所有曲面。它使用正交投影(详见4.7.1节)。这种类型的投影对于贴花非常有用(详见20.2节)。

由于edge-on to投影方向的曲面存在严重的变形,所以艺术家通常必须手动将模型分解为接近平面的部分。有一些工具可以通过展开网格或创建一组接近最优的平面投影或者其它方式来帮助最小化扭曲。目标是让每个多边形在纹理区域中获得差不多的尺寸,同时也尽可能保持网格连接。连通性很重要,因为采样伪影可以沿纹理的各个部分相交的边缘出现。一个具有良好展开效果的网格也可以使艺术家的作品更容易(参考文献[970、1345])。第16.2.1讨论了纹理失真如何对渲染产生不利影响。图6.6显示了用于创建图6.5中雕像的workspace。这个展开过程是一个更大的研究领域,网格参数化。感兴趣的读者可以参考Hormann等人在SIGGRAPH上的课程(参考文献[774])。

Texture

上图展示了雕塑模型的一些小的纹理,保存在2个大纹理中。右图展示了mesh是如何unwrap并显示在纹理上,以帮助纹理的绘制。(图片由Tito Pagan提供)

纹理坐标空间并非总是二维平面,有时是三维的。在这种情况下,纹理坐标被通过三维向量(u,v,w)表示,其中w是沿着投影方向的深度。还有一些系统可以使用四个坐标,表示为(s,t,r,q)(参考文献[885])。其中q为齐次坐标中的第四个值,就像电影或者幻灯机,投影纹理的大小随着距离的增加而增大。举个例子,可以将一种装饰性的聚光灯图案(gobo),投射到舞台或其他表面上。

还有一种重要的纹理坐标空间类型是方向性的,空间中的每个点都由一个输入方向访问。将这个空间可视化的方法是将其视为单位球体上的点,每个点上的法线表示在该位置访问纹理的方向。使用方向作为参数的最常见的纹理类型是cubemap(详见6.2.4节)。

同样值得注意的是,一维纹理图像也有自己的作用。比如,在地形模型上,颜色可以由海拔决定,例如:地势低的地方是绿色的,山峰是白色的。线条也可以由纹理,比如将雨水渲染成一组具有半透明图像纹理的长线。一维纹理还可以用于将一个值转换到另外一个值,比如作为lookup table使用。

由于可以将多个纹理应用于一个物体表面,因此可能需要定义多组纹理坐标。不管坐标值是如何应用的,其思想是相同的,这些纹理坐标是在整个曲面上被插值,并用于检索纹理值。但是,在插值之前,这些纹理坐标由相应的函数变换(warpmode)。

6.1.2 相应的函数变换(Corresponder)

Corresponder函数将纹理坐标转换成纹理空间位置,使得将纹理应用于物件表面时更加灵活。Corresponder函数的一个例子,就是使用API选择现有纹理的一部分进行显示,然后在后续的操作中将仅适用此子图像。

还有一种类型的Corresponder是矩阵变化,可以应用在VS或者PS上。这样可以在曲面平移、旋转、缩放、shearing或者projecting纹理。如第4.1.5节所述,transform的顺序很重要。不过令人惊讶的是,纹理的transform顺序必须与预期的顺序相反。这是因为纹理变化实际上影响的是决定图像显示位置的空间。图像本身并不是转换的对象,转换的是定义图像位置的空间。

还有一种corresponder函数控制图像如何被使用。我们知道图像会出现在(u,v)在[0,1]范围内的物件表面上。但如果在范围之外会发生什么?corresponder函数将用于决定将发生什么。在OpenGL中,这种corresponder 函数被称为“wrapping mode”,而DX中,这种类型的函数被称为“texture addressing mode”。常见的corresponder 函数如下:

  • wrap(DX),repeat(OpenGL),或者tile。图像将在物件表面上不断重复,从算法上来说,就是将纹理坐标中的整数部分舍弃。这种函数用于使得图像不断重复,覆盖满整个物件表面,通常被作为默认值。
  • mirror。图像在整个物件表面不断重复,但每重复一次会进行一次镜像(flipped)。举个例子,图像先按照正常方式从0到1,然后在从1到2的时候以反转的方式展现,然后2到3的时候又是正常方式,然后再反转,不断如此。这种方式会使得纹理边缘连贯。
  • clamp(DX),clamp to edge(OpenGL)。当数值超过[0,1]的时候,会被clamp在这个范围内。这样会导致图像纹理的边重复。当使用双线性插值,对纹理边缘进行采样的时候,这个函数可以被用于避免意外的从纹理的相反边缘提取到样本(参考文献[885])。
  • border(DX),clamp to border(OpenGL)。当纹理坐标超过[0,1]的时候,会使用一个单独被定义的边框颜色。这种方式可以被用于很好的将贴花渲染到单色的物件表面上。例如,纹理的边缘将通过这种边框颜色平滑的与周围混合。

如图6.7所示,这些函数可以根据不同的纹理轴分配不同的功能。比如,纹理可以沿着u轴重复,然后在v轴使用clamp的方式。在DX中,还有一个mirror once模式,该模式沿着纹理坐标的0值进行一次mirror,然后其余部分以clamp的方式,这种方式很适合对称贴花。

Texture

上图展示了,纹理的repeat、miror、clamp以及border四个函数。

(Patrick:上述这三种Corresponder函数,第一种就是tiling和offset,第二种没听过,不过不难理解,第三种就是wrapmode)

重复的平铺纹理是一种为场景添加更多视觉细节的廉价方法。然而,当贴图重复超过三次之后,眼睛就能挑出这些图案,从而使得并不令人信服。避免这种重复的常见解决方案就是将纹理与另一个非平铺的纹理相结合。正如Andersson(参考文献[40])所描述的商业地形绘制系统中那样,这种方法还可以进行扩展。在该系统中,根据地形类型、海拔高度、坡度等因素,将多个纹理进行组合。纹理图像还与几何模型(比如灌木丛和岩石)在场景中的放置位置相关联。

避免周期性的另外一个选择是使用shader来实现专门的Corresponder函数。这些函数随机的重新组合纹理图案或者tile。Wang tile就是这种方法的一个例子。Wang tile是一组边缘匹配的方形tiles。在采样过程中随机的选择tiles(参考文献[1860])。Lefebvre和Neyret(参考文献[1016])使用依赖纹理读取和表的方式实现了一种类似的函数,以避免模式重复。

(Patrick:这个第二种方法还真没看懂。)

最后一个Corresponder函数是隐式的,并根据图像的大小派生出来的。纹理通常都应用于(u,v)在[0,1]范围内。如砖墙示例所示,通过将该范围的纹理坐标乘以图像的分辨率,获得像素位置。能够在[0,1]范围内指定(u,v)值的优点是,可以使用不同分辨率的图像纹理,而无需更改存储在模型顶点的数值。

(Patrick:最后这一段貌似相当于啥也没写?)

6.1.3 纹理数值

当使用Corresponder函数生成纹理坐标后,就可以使用坐标去获取纹理值了。对于图像纹理,是通过访问纹理,从图像中检索纹素的方式来完成。第6.2节对改过程进行了详细的讨论。图像纹理在实时渲染广泛使用,除此之外还可以使用程序纹理。使用程序纹理的时候,根据纹理空间位置获取纹理值的过程不涉及内存查找,主要是通过函数计算。程序纹理在第6.3节中进行了说明。

最常见的纹理值是用于修改物件表面颜色的RGB值,类似的,还可以使用单个的灰度值。还有一种类型是RGBA,如第5.5节所述。alpha通常是颜色的不透明度,决定颜色可能会影响像素的程度。还可以存储任何其他值,比如表面粗糙度。还有许多其他类型的数据可以被存储在图像纹理中,第6.7节讲述bump map的时候就可以看到。

从纹理获取的值可以在使用前进行变换。这些转换在PS中执行。一个常见的示例是将数据从无符号范围(0到1)重新映射到有符号范围(-1,1),这就是当用颜色纹理存储normal时的做法。

6.2 图像纹理

在图像纹理处理中,二维的图像被影射到一个或者多个三角形的表面。我们已经完成了纹理空间位置的计算,下面将讨论从给定位置的纹理图像中获取纹理值的问题和算法。在本章的其余部分中,图像纹理被简称为纹理。此外,当我们在这里提到像素的单元格时,我们指的是围绕该像素的屏幕网格单元格。如第5.4.1节所述,像素实际上是一个颜色值,它可以(而且应该,为了更好的质量)由其相关网格单元外的样本一起计算得到。

在本节中,我们主要关注快速采样和过滤纹理图像的方法。第5.4.2节讨论了走样的问题,特别是在渲染对象的边缘。纹理也可能有采样问题,但它们发生在正在渲染的三角形的内部。

PS通过将纹理坐标值传递给API texture2D来访问纹理。这些值是将纹理坐标(u,v)通过Corresponder函数映射到[0,1]。GPU负责将该值转换为纹素坐标。在不同的API中,纹理坐标系有两个主要的区别。在DX中,纹理的左上角是(0,0),右下角是(1,1)。这种方案与存储其数据的图像类型相匹配,最上面一行为文件的第一行。在OpenGL中,(0,0)点位于左下角,与DX的方案沿着Y轴翻转。纹素有整数坐标,但是我们通常希望访问纹素之间的位置,并将相邻的数据混合。这就引出了一个问题,像素的中心点位置的浮点坐标是什么。Heckbert(参考文献[692])提出了两种可能:truncating(截断)和rounding(舍入)。DX 9将中心点定义为(0,0)点,这就是rounding。这种方案有点混乱,因为左上角的像素的左上角的点,也就是DX的原点,纹理坐标为(-0.5,-0.5)。DX10之后就更改为了OpenGL的方案,其中纹素的中心点为(0.5,0.5),这就是truncation,或者更精确的语言,floor,也就是将小数部分删除。floor是一个更容易理解的语言,比如像素(5,9),代表着u-坐标从5.0到6.0,v-坐标代表着从9.0到10.0。

在这一点上有一个值得解释的术语是dependent texture read依赖纹理读取,它有两个含义。第一种很适合移动设备。通过texture2D或者类似方法访问纹理的时候,每当PS计算纹理坐标,而非直接使用VS传入的纹理坐标的时候,就会出现dependent texture read(参考文献[66])。请注意,这意味着对传入纹理坐标的任意更改,甚至包括交换u、v值这样的简单操作。较旧的移动GPU(不支持OpenGL ES3.0)在PS中没有dependent texture read的时候运行效率更高,因为这样可以预读取纹素的数值。这个术语还有另外一个更古老的定义,针对早期的桌面GPU尤为重要。在这种情况下,当一个纹理坐标依赖于先前某些纹理值的时候,将发生dependent texture read。举个例子,一个纹理可能会改变着色法线,从而更改用于访问立方体贴图的坐标。这种功能在早期GPU是受限的,甚至是不被允许的。如今,这种读取依然会对性能产生影响,这取决于批处理中计算的像素数以及其他因素。更多信息见23.8节。

GPU中使用的纹理图像大小通常为pow(2,m)*pow(2,n)个纹素,其中,m和n是非负整数。它们被称为POT纹理。现代GPU支持任意尺寸的NPOT纹理,这使得任何图像都可以被当做纹理处理。但是一些旧的移动GPU不支持NPOT纹理的mipmap(详见6.2.2)。图形加速器对纹理尺寸有不同的上限。比如,DX12最多支持16384*16384的纹理。

假设我们有一个大小问256*256纹素的纹理,并希望将其作用在一个正方形上。加入屏幕上投影的正方形大小与纹理大致相同,正方形的纹理看起来几乎与原始图像相似。但是,如果投影的正方形覆盖的像素是原始图像的十倍(称为magnification放大倍数),或者投影的正方形只覆盖屏幕的很小一部分(minification缩小),会发生什么情况?答案是,这取决于你决定对这两种不同的情况使用哪种采样和过滤方法。

本章讨论的图像采样和滤波方法,是针对从纹理读取的值。但是,所需的结果是防止最终渲染图像中出现走样,这在理论上需要对最终像素颜色进行采样和过滤。这里的区别是过滤着色方程式的输入还是输出。只要输入和输出是线性相关的(比如颜色等输入),那么过滤单个纹理值相当于过滤最终颜色。但是,存储在纹理中的很多着色器输入值(比如法线和粗糙度等值)与输出具有非线性关系。标准纹理过滤额方法可能无法很好的处理这些纹理,从而导致走样,第9.13节讨论了此类纹理的改进方法。

6.2.1 放大倍数

在图6.8中,大小为48*48纹素的纹理被渲染到一个正方形上,而这个正方形在屏幕上的尺寸相当大,所以需要底层图形系统放大纹理。最常见的放大滤波技术是nearest(对应的滤波器为box,详见5.4.1)和bilinear双线性插值。还有cubic convolution三次卷积算法,使用4*4或5*5的纹素数组的加权和。这样可以获得更好的放大质量。尽管对cubic convolution(也被称为bicubic双三次插值)的本地硬件支持并不普遍,但它可以在着色器中被执行。

在图6.8的左边,使用了nearset算法。这种放大技术的一个特点是单个纹素可能会变的明显。这种效果被称为像素化,之所以出现这种效果,是因为在放大的时候,改方法将最接近每个像素中心的纹素取了出来,从而产生块状外观。虽然这种方法的质量有时很差,但它针对每个像素只需要提取一个纹素。

在中间图像中,使用双线性插值(有时被称为线性插值)。对于每个像素,这种过滤会找到四个相邻的纹理,并在二维中进行线性插值,以得到像素的混合值。这样得到的结果会比较模糊,由于使用nearest方式产生的缺陷也都消失了。可以尝试一下以斜视的方法观察左边的图像,这种观察方式和低通滤波器的效果差不多,并且可以更清楚的看清楚脸部。

Texture

将48*48尺寸的纹理放大到320*320像素。左图:使用nearest滤波,针对每个像素使用最近的纹素。中图:使用双线性滤波,针对每个像素使用最近的四个纹素的加权平均值。右图:使用cubic滤波器,针对每个像素使用最近的5*5个纹素的加权平均值。

回到170页的砖墙纹理示例:假如得到一个纹理坐标(u,v)为(81.92,74.24)。我们在这里使用OpenGL坐标系,也就是左下角为纹素的原点,这样的话与标准笛卡尔坐标系匹配。我们的目标是在最近的四个纹素之间进行插值,使用纹素中心来定义一个纹素的坐标系。如图6.9,为了找到最近的四个像素,我们从样本位置减去像素中心小数(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为整数,并返回纹素的颜色。(u’,v)位置的双线性插值颜色可以通过两步过程来计算。首先,底部的纹素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)(图6.9中底部绿色圆圈)。对于顶部,得到(1-u’)*t(x,y+1)+u’*t(x+1,y+1)(顶部绿色圆圈)。然后这两个值被垂直插值(使用v’),所以坐标(u,v)处的双线性插值颜色为

Texture Texture

双线性插值。所涉及的四个纹素由左侧的四个正方形表示,纹素中心为蓝色。右边是由四个纹素的中心组成的坐标系。

直观的说,靠近我们样本位置的纹素将会更多的影响最终值。这和我们从等式中看到的一样。(x+1,y+1)处的右上角纹素对(u’,v’)有影响。需要注意对称性:右上角的影响等于左下角和采样点形成的矩形的面积。回到我们的例子,这意味着从这个纹素检索到的值将乘以0.42*0.73,也就是0.3108。从这个纹素出发,顺时针方向,右下角纹素的影响值为0.42*0.26,左下角的影响值为0.58*0.26,左上角的影响值为0.58*0.74。这四个权重相加为1.0。

一个常见的解决方案是使用细节贴图。这些贴图可以用来展示精细的表明细节,从手机上的划痕到地形上的灌木丛。这些细节以不同的比例,作为单独的纹理叠加到放大的纹理上。细节纹理的高频重复图案,结合低频放大纹理,具有与使用单一高分辨率纹理相似的视觉效果。

双线性插值在两个方向上线性插值。但是,也并非一定要线性插值。假设纹理由棋盘格图案中的黑白像素组成。使用双线性插值在纹理上提供不同的灰度样本。通过重新映射,比如,低于0.4的所有灰度都被认为是黑色,高于0.6的所有灰度都被认为是白色,中间的灰度被拉伸以填充间隙,这样纹理看起来更像是一个棋盘格,同时也在纹理之间提供了一些混合。见图6.10。

Texture

使用相同的2*2棋盘格纹理,使用nearest、双线性、通过重新映射介于两者之间。需要注意的是,使用nearest采样的话产生的正方形大小略有不同,因为纹理和图像网格并非完全匹配。

使用一个更高分辨率的纹理也会产生类似的效果。比如,假如每个棋盘方格是有4*4纹素,而非1*1纹素构成。在每个棋盘格中心周围,插值的颜色将是完全黑色或者白色。

在图6.8的右边,使用了一个双三次滤波器,剩下的块状物基本上被去除了。但是需要注意的似乎,双三次滤波器比双线性滤波器还要昂贵。然而,很多高阶滤波器可以表示为多次线性插值(参考文献[1518])(详见17.1.1节)。因此,在GPU硬件中针对纹理单元中的线性插值可以通过多次lookup实现。

如果认为双三次滤波器太昂贵,Quilez(参考文献[1451])提出了一种使用平滑曲线在一组2*2纹素之间插值的简单技术。我们先来看曲线,然后再说具体技术。这两种常见曲线是smoothstep曲线和quintic五次曲线(参考文献[1372])

Texture Texture

对应许多其它希望从一个值平滑插值到另外一个值的情况,这些都很有用。smoothstep曲线具有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')中,(u',v')在[0,1]范围内。然后将(u',v')按照(tu,tv)=(q(u'),q(v'))转换后,依然在范围[0,1]内。最后,减去0.5,再讲整数部分加回来,得到一个新的uv坐标,然后除以纹理尺寸。这个时候就得到了一个新的纹理坐标用于GPU提供的双线性插值。请注意,这种方法将在每个纹素中给出一个数值,这意味着,加入纹素位于RGB空间中,这种插值将给出平滑但仍然是楼梯式的外观。这可能并不总是需要的。如图6.12。

Texture

上图为放大一维纹理的四种不同方法。橙色圆圈表示纹素的中心以及纹素值(高度)。从左到右:nearest、双线性插值、在每队相邻纹理之间使用五次曲线,以及使用三次插值。

6.2.2 缩小

当对纹理进行缩小的时候,几个纹素可以覆盖一个像素的单元格。如图6.13所示。若要针对每个像素获取正确的颜色值,需要计算出每个纹素对像素的影响。然而,很难精确的确定该像素所有周边纹素对其的影响,而且在实时计算中也不可能完美的做到这一点。

Texture

由于这个限制,GPU上使用了几种不同的方法。一种方法是使用nearest,工作原理与相应的放大滤波器完全相同,即,它选择像素单元中心的纹素。这个方案可能会导致严重的锯齿。在图6.14中,最上面的图片使用了这个方案。可以看到,在朝向地平线的方向,会出现伪影,因为仅选择影响像素的多个纹素中的一个用来表示。当表面相对观察者移动的时候,这种伪影会更加明显,是temporal aliasing的一种表现。

另一种经常使用的滤波器是双线性插值,与放大滤波器中原理也一样。改滤波器仅略优于nearest。它混合了四个纹素,而并非只用一个,但是当一个像素受到四个以上纹素影响的时候,这种滤波就会失败并产生锯齿。

Texture

上图中顶部的图像使用点采样(nearest),中图使用mipmap,底图使用summed area tables面积总和表进行渲染。

可能还会有更好的解决方案。如第5.4.1节所述,锯齿问题可以通过采样和滤波技术来解决。纹理的信号频率取决于纹素在屏幕上的间距。由于Nyquist极限,我们要确保纹理的信号频率小于等于采样频率的一半。举个例子,假如一个图像是有黑白相间的线组成,相隔一个纹素。也就是波长为2个纹素(从黑线到黑线),因此频率是1/2。要在屏幕上正确显示这个纹理,采样频率至少要2*1/2,也就是每个纹素对应一个像素。因此,对于一般的纹理,每个像素最多应该只对应一个纹素,以免出现锯齿。

为了达到这个目的,要么提高像素的采样频率,要么降低纹理的信号频率。前面一章聊到的抗锯齿方法都是用于提高像素的采样频率。然而,这样提高的采样频率也很有限。为了更充分的解决这个问题,人们开发了各种纹理细化算法。

所有纹理抗锯齿背后的思想都是相同的:预处理纹理并创建数据结构,这将有助于计算一组纹理对像素影响的快速近似值。对于实时工作,这些算法具有使用固定时间和执行资源的特点。通过这种方式,每个像素采集固定数量的样本,并组合以计算(潜在的巨大)数量的纹素的影响。

Mipmapping

最流行的纹理抗锯齿方法叫做mipmapping(参考文献[1889])。它以,某种形式在现代生产的所有图形加速器上实现。mip代表着multum in parvo,在拉丁语中指的是many things in a small place。这是一个很好的名字,在这个过程中,原始纹理被反复过滤成更小的图像。

当使用mipmapping中的缩小滤波器的时候,在实际渲染之前,原始纹理会被一组较小版本的纹理代替。纹理(lod0)会被降采样到原始区域的1/4,每个新的纹素值通常计算为原始纹理总四个相邻纹素的平均值。新的一级纹理有时会被称为原始纹理的子纹理。递归执行缩减,直到纹理的一个或两个维度等于一个纹素。这个过程如图6.15所示。作为一个整体的图像集通常被称为mipmap chain。

Texture

将金字塔底部的原始图像(lod0),取2*2区域的平均值,作为下一级的纹素值,以此形成mipmap。垂直轴是第三个纹理坐标d。在这个图中,d不是线性的,它是用来测量使用哪两个纹理级别用于插值。

得到高质量mipmap的两个重要因素是良好的滤波器和gamma矫正。形成mipmap的常用方法是取每个2*2的纹素集,并对它们进行平均以获得新的纹素值。使用的滤波器是box 滤波器,box滤波器可能是最差的滤波器之一。这可能会导致质量差,因为它具有不必要的模糊低频信号的效果,同时保留了一些导致锯齿的高频信息(参考文献[172])。最好使用Gaussian、Lanczos、Kaiser或者类似的滤波器,这些都有一些快速的免费的源代码(参考文献[172、1592])。有些GPU还支持一些特有的API,支持更好的滤波算法。在纹素的边缘,需要特别注意该纹理是repeat还是单个副本

(Patrick:这个我就遇到过一个mipmap没有计算好的情况,脚底的贴花,wrap mode为clamp to edge,按说应该只显示纹理中间的圆圈,边缘处全是空白,但是有时候边缘会出现边框效果,原因就是纹理的mipmap层没有计算好,导致某个mipmap层边框是有颜色的,然后使用tex2d(u,v,0,0)即可,或者该贴花纹理不创建mipmap即可)

对于在非线性空间编码的纹理(比如大多数颜色纹理),在过滤时如果忽略gamma矫正,滤波器将修改mipmap级别的亮度(参考文献[173、607])。当你离物体越来越远,这些不正确的mipmap就会被用到,使得物件看上去偏暗,对比度和细节也会受到影响。因此,将这些纹理从sRGB转换到线性空间(详见5.6节),并在该空间执行所有mipmap过滤,并将最终结果转换回sRGB颜色空间以供保存,这个步骤非常重要。大多数API都支持sRGB纹理,因此可以在线性空间中正确生成mipmap并将结果存储在sRGB中。在使用sRGB纹理的时候,首先将其转换成线性空间,以便正确执行放大和缩小操作。

如前所述,一些纹理与最终着色颜色可能会有一些非线性关系。这个会影响到过滤,而由于mipmap的生成涉及到了大量的像素过滤,所以mipmap的生成对这个问题也特别敏感。为了获得最佳结果,通常需要专门的mipmap生成方法。此类方法详见9.13节。

在处理纹理的时候进行访问的过程其实很简单。通过屏幕中一个像素确定对应纹理中的一个区域,将像素区域投影到纹理上(如图6.16),投影的这个区域可以包括一个或者多个纹素。在这里我们使用像素的单元格边界并不是严格正确的,而是用来简化表示。单元格外面的纹素也会影响到像素的颜色,可以参考第5.4.1节。确定了关联的纹素后,下一个要确定的就是每个纹素对像素的影响。有两种常见的计算d的方法(OpenGL中称为λ,也被称为纹理的level of detail)。一种是通过像素单元形成的四边形的长边来近似像素的覆盖范围(参考文献[1889])。另外一种是使用四个差分的最大绝对值 ∂u/∂x, ∂v/∂x, ∂u/∂y, ∂v/∂y(参考文献[901,1411])。每个差分是对相对于屏幕轴的纹理坐标变化量的度量。例如,∂u/∂x是一个像素沿着x屏幕轴的u纹理变化量。有关这些方程的更多信息,可以参考Williams的文章(参考文献[1889])、Flavell的文章(参考文献[473])、Pharr 的文章(参考文献[1411])。McCormack 等人(参考文献[1160])提到,如果使用最大绝对值的方法,会引入锯齿,并给出了另外一个公式。Ewins 等人(参考文献[454])也讨论到了几种质量差不多的算法对应的硬件成本。

Texture

上图左侧为一个正方形像素单元及其纹理视图,右侧为像素单元到纹理本身的投影。

这些渐变值在SM3.0及以上均可访问。但是,由于它们基于相邻像素之间的差异,所以,无法在受动态分支(见3.8节)影响的PS中访问它们。在这种情况下访问贴图(比如循环中),必须提前计算导数。需要注意的是,VS无法访问渐变值,所以渐变值或者level of detail信息,需要在VS本身中进行计算,并在使用顶点纹理的时候,提供给GPU。

(Patrick:这里貌似有个知识点,动态分支访问贴图还有问题?相应的动态分支就访问不了ddx、ddy了?)

计算坐标d的目的是确定沿着mipmap的金字塔轴采样的位置。见图6.15。目的是像素与纹理的比例为1:1,已达到Nyquist速率。这里的原则是,当像素单元格包含更多的纹素的时候,可以通过增加d的方式,访问更小更模糊版本的纹理。(u,v,d)三元素被用于访问mipmap。值d类似于纹理级别,然而d并不是整数值,而是具有级别之间距离的分数值。对d位置上方和下方的纹理进行采样。(u,v)位置用于从这两个纹理级别中的每个级别检索双线性插值样本。然后根据每个纹理级别到d的距离,对生成的样本进行线性插值。整个过程被称为三线性插值,并按照逐像素执行。

用户可以通过level of detail bias控制d坐标。这个bias会和d相加,因此会影响对纹理mipmap的选择。如果使用金字塔更上层(增加d),纹理将会看起来更模糊。根据纹理的类型和使用方式的不同,应该选择不同的lod bias。比如,一开始有些模糊的图像可以使用负偏移,而纹理中出现锯齿的时候需要使用正偏移。可以为整个纹理或者像素着色器中的每个像素指定偏移。为了更精确的控制,用户可以访问用于计算d的导数。

(Patrick:感觉可以通过这个来处理低端手机,使其访问低mipmap,但是这样貌似会影响纹理预读取?)

mipmap的好处在于,不需要单独为每个像素处理所有相关的纹素,而可以直接访问和插值预计算得到的纹素集。然而,mipmap有几个缺陷(参考文献[473])。最主要的一个就是过度模糊。假如一个像素在u方向覆盖了大量的纹素,而在v方向只覆盖了少量的纹素。这种情况通常发生在观察者沿着纹理的边缘观察。这样,可能需要沿着纹理的一个轴缩小,而沿着另外一个轴放大。但是由于访问纹理的时候是检索纹理上的方形区域,而不可能去检索其矩形区域。为了避免锯齿,我们一般选择低mipmap层级的,这将导致检索到的样本往往比较模糊。这种效果可以在6.14的mipmap图像中看到。右侧的线就是过度模糊了。

Summed-Area Table

避免过度模糊的一个方法是summed-area table(SAT)(参考文献[312])。要使用这个方法,首先需要创建一个数组,该数组的大小和纹理一样,但需要更高的精度用来存储颜色(比如,r、g、b通道各使用16位以上)。在该数组的每个位置中,必须计算并存储该位置与原点(0,0)纹素形成的矩形中所有纹素的和。在采样纹理的时候,像素将被投影到纹理上,形成一个矩形。然后通过summed-area table来决定该矩形的平均颜色,然后被用作该像素的纹理颜色。该平均值是通过图6.17中对应的矩形纹理坐标计算得到。计算公式为6.3。

Texture

如上图所示,像素被投影到纹理上,形成一个矩形。矩形的四个角被用于访问summed-area table。

Texture

这里,X和Y是矩形纹素坐标,S(x,y)为该纹素对应的summed-area的值。这个公式的原理,是将从右上角到原点的全部区域值的和,通过减去相邻角的贡献来减去A和B的区域。由于C区域被减去了两次,所以通过加上左下角的区域再加回来。注意(Xll,Yll)是C区域的右上角,也就是说,(Xll+1,Yll+1)为边框的左下角。

图6.14也展示了使用summed-area table的结果。可以看到右边的水平线还是挺清晰的,但是中间的对角线交叉线依然很模糊。这是因为,当沿着纹理的对角线去查看纹理的时候,会产生一个大的矩形,这样就会将很多纹素被用于计算像素。可以想象一下,像素投影了一个细长的矩形,投影到了图6.17的纹理上。这样的话将返回整个纹理矩形的平均值,而不仅仅是像素单元格内的平均值。

(Patrick:最后一句,我觉得说的有问题。像素单元对应了纹理中的一块细长的矩形区域,那么用summed-area就会将很多纹素合在一起计算。除非不用summed-area这个方式,就不会将那么多纹素合在一起计算了。)

summed-area table是所谓各向异性滤波算法(参考文献[691])的一种方式。这种方式针对非正方形区域检索纹素值。然而,SAT能够在水平和垂直方向上有效的实现这一点。需要注意的是,对于大小为16*16或者更小的纹理,summed-area table至少需要2倍内存,纹理更大,需要的精度就更高。

summed-area table以合理的总内存成本提供了更高的质量,在现代GPU中可以实现(参考文献[585])。改进的过滤算法对高级渲染的质量至关重要。比如,Hensley等人(参考文献[718,719])提供了一种有效的实现方法,展示了summed-area采样如何改善光泽反射。还有一些其他算法也可以通过SAT改进,比如DOF(参考文献[719]),shadow map(参考文献[988]),还有模糊反射(参考文献[718])。

(Patrick:这是个有意思的技术,但是缺点很明显,不过结合VT,感觉还是有一些想象空间的。参考安柏霖大大的文章)

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

对于当前的图形硬件,提升纹理采样最常见的方法是重用现有的mipmap硬件。基本思想是根据像素单元格对纹理上的方形进行多次采样,然后将样本合并。如上所述,每个像素都在所有mipmap层有一个与其对应的方形区域。下面,我们将不只是通过一个mipmap层的采样来实现,而是使用若干个方形来一起进行处理。先用较短边来确定d(与mipmap不同,mipmap中为了避免锯齿,所以会使用长边来确定),这样会得到一个高mipmap层,这样模糊会消失,但是会有锯齿。然后使用四边形较长边创建一个平行线穿过方形。当各向异性的数量在1:1和2:1之间的时候,沿着这条线使用两个样本(如图6.18)。各向异性比例越高,将沿着这条线使用越多的采样样本。

Texture

如上图所示,各向异性滤波。像素在纹理上投影除了一个矩形,然后在长边上形成了一条各向异性的线。

这个方案允许各向异性的线可以沿着任意方向,因此不受summed-area table的影响。且不需要比mipmap更多的内存,因为它就是在mipmap基础上进行采样运算的。各向异性滤波的实例如图6.19所示。

Texture

上图为mipmap和各向异性的对比。左边是trilinear mipmap,右侧为16:1的各向异性。在水平方向上,各向异性提供了更清晰的结果,且锯齿也最小(图片来自与three.js中webgl materials纹理各向异性(参考文献[218]))

这种沿着轴采样的想法最初是由Schilling等人提出,使用在它们的Texram动态存储设备中(参考文献[1564])。Barkans在Talisman系统中也使用了该算法(参考文献[103])。McCormack等人也使用了类似的算法,被称为Feline(参考文献[1161])。Texram的原始公式是将沿着各向异性轴的样本(也被称为探针),赋予相同的权重。Talisman中,轴两端的两个探针的权重只有其他采样点权重的一半。Feline针对这些探针使用了高斯滤波器。这些算法可以得到的结果,已经可以接近比如Elliptical Weighted Average(EWA)椭圆加权平均滤波器等高质量的采样算法所能得到的效果。EWA将像素影响的区域转换为了纹理上的椭圆,并通过滤波核对椭圆的纹理进行加权(参考文献[691])。Mavridis和Papaioannou提出了几种在GPU中使用shader代码实现EWA过滤的方法(参考文献[1143])

6.2.3 Volume Textures

图像纹理可以从维度上进行扩展,扩展成三维图像,并通过(u,v,w)或者(s,t,r)数值访问。比如,医学成像数据可以生成三维网格,通过移动该网格中的多边形,可以查看这些数据的二维切片。一个相关的想法,是通过这种方式表示体积光。通过查看体积内某一点的值以及灯光的方向,就可以算出该点的照明。

大多数GPU支持体积纹理的mipmap。然而在体积纹理中,即使是单层mipmap做过滤,都要使用到三线性插值,那么在mipmap各个级别之间的过滤就要涉及到Quadrilinear四线性插值了。这样的话,可能会需要16个纹素来功能计算出来结果,这样的话就可能会导致精度问题,这个问题可以通过使用更高精度的体积纹理来解决。Sigg和Hadwiger(参考文献[1638])讨论了这个以及体积纹理相关的一些其他问题,并提供了过滤等操作的一些有效方法。

尽管体积纹理需要更大的存储空间要求和更昂贵的过滤机制,但它们确实有一些独特的优势。由于可以直接通过三维坐标作为纹理坐标,那么就可以跳过为三维网格进行二维uv拆分的复杂过程。这也就避免了使用二维uv导致的变形和接缝问题。体积纹理也可被用于表示材质(比如木材或大理石)的体积结果。具有这种纹理的模型,看上去就好像是使用这种材质雕刻而成。

然而使用体积纹理用于表面渲染是非常低效的,因为绝大多数样本都没被使用到。Benson和Davis(参考文献[133])以及DeBry等人(参考文献[334])讨论了如何在稀疏八叉树结构中存储纹理数据。该方案非常适合交互式三维渲染系统,因为曲面在创建的时候不需要为其制定显示纹理坐标,而可以通过八叉树将纹理细节保持在所需的任何级别。Lefebvre和Hoppe(参考文献[1018])讨论了一种将稀疏体积数据打包成明显较小纹理的方法。

(Patrick:这一段我想了一下大概是这个意思,比如一个可交互破碎的石头,那么创建石头的时候不需要设置其纹理坐标,而石头的样子其实是绘制到3D纹理中的,那么石头无论如何破碎,根据石头破碎后的mesh算出纹理坐标,去3D纹理采样,都能得到一个正确的结果)

6.2.4 Cube Maps

还有一种纹理类型是CubeMap,它有6个正方形纹理,每个都与立方体的一个面相关联。cubemap是通过一个三分量纹理坐标向量来访问,该向量从立方体中心向外发射。光线和立方体交点是通过下述方法计算得到。首先,根据纹理坐标的最大绝对值选择对应的面(例如,向量(-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.8)。cubemap对于那些需要根据方向采样值的情况很有用,比如最常用于环境映射(详见10.4.3节)。

6.2.5 Texture Representation

当一个应用程序需要处理很多纹理的时候,有几种方法可以提高性能。第6.2.6节中会介绍纹理压缩,本节将主要介绍texture atlas,texture array以及bindless texture,所有的这一切都是为了避免渲染中切换纹理的消耗。在第19.10.1和19.10.2节中会介绍texture streaming以及transcoding转码。

为了使得GPU可以在更好的批量工作,最好尽量减少状态的改变(第18.4.2节)。为此,可以将多个图像放入到一个更大的纹理中,被称为texture atlas。如图6.20所示。注意,子纹理的形状可以是任意的,如图6.6所示。Noll和Stricker(参考文献[1286])介绍了针对atlas中子纹理放置方式的优化。但是,在对mipmap的生成和采样的时候,需要多多注意,因为mipmap的上层可能包含几个独立、不相关的形状。Manson和Schaefer(参考文献[1119])提出了一种考虑曲面参数来优化mipmap创建的方法,这种方法可以得到更好的结果。Burley和Lacewell(参考文献[213])提出了一个被称为Ptex的系统,其中细分曲面中的每个四边形都有自己的小纹理。这样做的好处是避免针对mesh指定唯一的纹理坐标,并且在texture atlas的接缝部分没有瑕疵(Patrick:我是这么理解的:就好比tilling和offset,mesh直接通过定位与子纹理关联,而与大纹理实际没有直接关系。mipmap在生成的时候也考虑到了子纹理的概念,所以生成的时候可以分开生成,然后再组合在一起)。为了能跨四边形进行过滤,Ptex使用了邻接数据结构。由于最初的定位是产品级渲染,Hillesland(参考文献[746])提出了packed Ptex,将每个face的子纹理放到一个texture atlas中,然后使用padding调整face来在过滤的时候避免出现问题。Yuksel(参考文献[1955])提出了mesh color texture,改进了Ptex。Toth(参考文献[1780])为一个类似于Ptex的系统提供了一个高质量的过滤算法,方法是在tap滤波的时候,丢弃掉范围[0,1]之外的数据。

使用atlas的时候有一个难点,就是filter mode(wrapping/repeat/mirror),这个设置是针对整个纹理而非子纹理。另外,在对atlas创建mipmap的时候也会出现问题,因为这个时候子纹理可能会渗透到另外一个子纹理中。然而,这个是可以避免的,比如可以针对每个子纹理生成mipmap,然后将其放到一个大的texture atlas中,每个子纹理使用POT的尺寸。

有个很简单的办法去解决上述问题,那就是使用texture array,这样就可以避免由于mipmap和filter mode带来的种种问题(参考文献[452])。可以参考图6.20。纹理数组中的所有子纹理,都需要使用相同的维度、格式、mipmap、MSAA。(Patrick:居然没提到尺寸)与texture atlas一样,一个texture array只会被设置一次参数,然后在shader中通过index访问数组中的每个元素。这种方式会比针对每个子纹理去bind要快5倍。(参考文献[452])

Texture

上图中的左侧,是将9个小图片组合成为一个单一的大图片,texture atlas。右侧为,一种更现代的做法是将较小的图片设置为纹理数组,这个特性在大多数API中都有。

还有一种避免状态切换的API,bindless texture(参考文献[1407])。如果没有bindless texture,纹理会被通过API绑定到一个纹理单元上。然而纹理单元的数量是有上限的,所以使得很多问题就复杂化。驱动需要被用于确保纹理在GPU端已经准备好了。然而对于bindless texture,纹理的数量是没有上限的,因为每个纹理都会被通过一个64位指针(也可以称为句柄)与其数据结构相关联。这些句柄可以被通过很多不同的方式访问。比如,通过uniform、varying、其他纹理、或者SSBO(shader storage buffer object)。应用程序本身需要被用于确保纹理在GPU端已经准备好了。bindless texture避免了驱动程序中的bind开销,这使得渲染速度加快。

(Patrick:OpenGL ES没有bindless texture?貌似真没见过)

6.2.6 压缩纹理

有一种直接可被用于解决内存、带宽、缓存问题的方案,是fixed-rate 纹理压缩(参考文献[127])。GPU可以随机解码压缩纹理,这样的话,纹理可以需要更少的纹理内存,增加有效缓存,纹理的使用率也会提高,节省更多的带宽。这样就可以通过压缩纹理使用大纹理。比如一个非压缩的纹理,每个纹素使用3byte的话,512的贴图需要768KB。如果使用压缩纹理,当压缩比是6:1的时候,1024的贴图只用512KB。

图像文件格式(比如JPEG和PNG)中使用了多种图像压缩方法,但是在硬件中实现这些方法的解码成本很高(有关纹理转码相关的信息,请参见19.10.1节)。S3开发了一个被称为S3 texture compression(S3TC)的方案(参考文献[1524]),该方案被选为DX的标准,并被称为DXTC,在DX10中,它被称为BC(block compression块压缩)。此外,它也存在与OpenGL的标准中,被几乎所有的GPU所支持。它的优点是创建了固定大小的压缩图像,具有独立的编码片段,并且解码简单(因此速度快)。图像的每个压缩部分都可以独立处理,不需要共享查找表或者其他依赖项,从而简化了解码。

DXTC/BC压缩方案有7种变体,它们之间有一些共性。编码是在4*4 纹素的块上完成,也被称为tile。每个块单独编码,编码是基于插值的。对于每个编码,会保存两个参考值(比如颜色)。块中的16个纹素,每个都会有一个插值因子。将使用它在两个参考值之间选择一个值。比如,从两个存储的颜色中插值颜色。压缩源于仅存储两种颜色以及每个纹素的索引值。

表6.1中总结了7种变体的区别。DXT为DX9中的名字,BC为DX10以及更高版本中的名字。如表中所示,BC1具有2个16位参考RGB值(RGB565),每个纹素具有一个2位的插值因子,可用于从一个或者两个参考值中进行选择(DXT1还有一种模式,针对半透明像素会保留四个可能的插值因子,对应的参考值有3个,2个参考值和它们的平均值)。若与未压缩时的24位RGB值相比,纹理压缩比为6:1。BC2和BC1使用相同的颜色编码,但是为了alpha值,每个纹素增加4位,对于BC3,与BC1使用相同的额颜色编码。此外,使用2个8位参考值以及每个纹素3位插值因子对alpha值进行编码。每个纹素可以选择一个参考alpha或者中间值。BC4只有一个通道,编码方式等同于BC3中的alpha。BC5有2个通道,每个通道都按BC4的方式编码。

Texture

纹理压缩格式。所有的这些压缩格式都是针对4*4的块进行压缩。storage列显示每个block存储多少bytes(B)的内容,以及每个纹素多少bits(bpt)。Ref Colors那一列先展示了通道,然后展示了每个通道的位数。比如,RGB565表示红色和蓝色为5位,绿色为6位。

BC6H是用于HDR(high dynamic range高动态范围)纹理,每个纹理的RGB通道都有16位浮点值。此压缩模式使用16字节,也就是8bpt。它有一种模式用于单行(类似上面的技术),还一种模式用于双行,其中每个块可以从一组小分区中进行选择。两种参考颜色也可以进行增量编码以获得更好的精度,并且根据所使用的的模式,也可以使用不同的精度。在BC7中,每个块可以有1-3行,并存储8bpt。目标是得到RGB8或者RGBA8纹理的高质量压缩纹理。它与BC6H共享很多属性,但它是LDR纹理的格式,而BC6H是HDR格式。注意,BC6H和BC7在OpenGL中被称为BPTC Float和BPTC。这些压缩技术可以应用于cubemap或者volume texture,就和两维纹理一样。

这些压缩方案的主要缺点是有损。也就是说,通常无法从压缩版本还原出原始图像。毕竟在BC1-BC5的情况下,每个像素仅用4-8bpt表示。如果一个tile中由大量不同的值,损失就会出现。而在实际应用中,如果正确使用,这些压缩方案通常是可以给出能接受的图像保真度。

BC1-BC5的一个问题是一个块中的所有颜色都位于RGB空间中的一条线上。比如,RGB不能在单个块中表示。而BC6H和BC7支持多条线,这样可以提供更高的质量。

OpenGL ES中,包含了一种称为Ericsson texture compression(ETC)的压缩算法(参考文献[1714])。这个方案与S3TC特点相同,即快速解码、随机存取、no indirect lookups、fixed rate固定压缩比。它将一个4*4的纹素块压缩成额64位,即每个纹素4位。基本思想如图6.21所示。每个2*4块(或者4*2块,取决于哪个质量好)存储一个基本色。每个块还从一个小型静态查找表中选择一组4个常量,块中的每个纹素都可以选择添加此表中的一个值,用于修改每个像素的亮度。图像质量和DXTC差不多。

Texture

如上图所示,ETC将一块中的像素编码成一个颜色,然后逐像素的设定luminance值,然后再根据这些还原出最终纹素颜色(图片由Jacob Strom提供)

OpenGL ES3.0中引入了ETC2(参考文献[1715]),相对于ETC来说,根据之前的一些无用模式,新增了很多模式。无用模式是指一个压缩representation和另外一个压缩representation解压出来的图像一样。例如,在BC1中,将两个ref颜色值设置成相同是没用的,这样与只有一个ref颜色值的结果相同。在ETC中,一种颜色可以用带符号的数字从第一种颜色进行增量编码,因此计算可以溢出或者下溢。这种情况被用来暗示压缩模式。ETC2增加了两个新的模式,每个块都有四种不同的颜色,然后使用RGB空间的一个平面来处理平滑过渡。EAC(Ericsson alpha compression)(参考文献[1868])对单通道的图像进行压缩(比如alpha通道)。这个压缩模式和ETC有点想,不过只针对单通道图像,压缩后的结果每个像素存4bpt。它可以被选择与ETC2结合。此外,还可以用两个EAC通道压缩法线(下面有关于这个主题的更多内容)。所有的ETC1、ETC2和EAC都是来自OpenGL 4.0 main spec、OpenGL ES3.0、Vulkan、Metal。

对normal map(将在6.7.2节详细讨论)的压缩需要特别注意。前面讨论了关于RGB图像的压缩往往不适合normal map。针对法线的压缩通常会利用法线的长度为单位长度,且默认Z通常通常为正(切线空间法线向量的合理假设)。这样的话只需要保存法线的x、y通道即可,然后z通道可以通过公式计算得到

Texture

这样的处理本身也就是一种压缩,因为这样的话只保存两个值,而非三个值。由于大部分GPU都不支持三分量纹理,这样也就避免了浪费一个分量(否则必须在第四个分量中打包另外一个信息通道)。进一步的压缩就是将x和y分量存储在BC5或者3Dc格式的压缩纹理中,如图6.22所示。由于每个block的参考值定义了x和y分量的最小和最大值,也就相当于在xy平面定义了边界框。三位插值因子允许在每个轴上选择8个值,因此边界框会被分为8*8网格。或者,也可以使用两个EAC通道(X和Y),然后按上述定义计算Z。

在不支持BC5/3Dc或者EAC的硬件上,一个常见的回退(参考文献[1227])是使用DXT5格式纹理,将两个组件保存在绿色和alpha通道上(因为这两个通道的精度最高)。其他两个component则没被使用。

PVRTC(参考文献[465])是Imagination Technologies硬件PowerVR上提供的一种纹理压缩格式,它最广泛的用途是用于iphone和ipad。它提供每纹素2位和4位的方案,针对4*4的纹素块进行压缩。核心思想是提供图像的两个低频(平滑)信号,这两个信号是通过相邻的纹素数据块插值得到,然后在图像上的两个信号之间使用时间每个像素1-2位的因子去插值。

ASTC(Adaptive scalable texture compression)(参考文献[1302])的不同之处在于它将n*m个纹素快压缩成28位,块的大小从4*4到12*12不等,这样会导致不同的压缩比,从0.89bpt到8bpt。ASTC使用一系列的技巧来表示索引,每个块都可以使用不同的行数和端点进行编码。此外,ASTC可以处理1-4通道的纹理,以及LDR、HDR的纹理。ASTC被OpenGL ES3.2及更高版本所支持

所有的压缩纹理都是有损的,压缩一个纹理,可能会根据质量不同花费不同的时间。可以花费几秒甚至几分钟,得到更高的质量,这种方式通常作为脱离预处理来完成,存储起来供以后使用。或者,可以花费几毫秒,得到一个质量较低的,但是纹理可以压缩后在实时渲染中立即使用。举个例子,由于云层可能一直在动,所以每隔一秒重新生成一个天空盒(第13.3节)。解压速度非常快,因为是在fixed-function的硬件中完成的。这种差异被称为数据压缩不对称,在这种情况下,压缩确实需要比解压缩需要更长的时间。

Kaplanyan(参考文献[856])提出了几种方法,可以提高压缩纹理的质量。对于包含颜色和法线的纹理,建议每个component使用16位。对于颜色纹理,使用直方图renormalization(在这16位上),然后使用着色器中逐贴图的scale和bias常量进行反转。直方图normalization是一种将图像中使用的值扩展到整个范围的技术,是一种有效的对比度增强方法。每个分量使用16位可以确保renormalization后的直方图没有未使用的缝隙,从而减少许多纹理压缩方案可能引入的带状伪影。如图6.23所示。此外Kaplanyan建议如果75%以上的像素高于116/255,则对纹理使用线性空间,否则将使用sRGB。对于法线贴图,他还注意到BC5/3Dc在压缩x的时候,完全不考虑y,这意味着很难获得最佳法线。所以,他建议对法线使用以下误差度量:

Texture

其中n为原始法线,nc为该法线对应的压缩,然后再进行解压缩。

Texture

上图显示了,在纹理压缩过程中,每个component使用16位而非8位的效果。从左到右:原始纹理,根据每个component 8位压缩出来的DXT1,根据每个component 16位压缩出来的DXT1,并在shader中进行renormalization操作。这些图片都使用了强光渲染,以更清楚的显示效果(图片由Anton Kaplanyan提供)

需要注意的是,可以在不同的颜色空间总压缩纹理,这可以用来加速纹理的压缩,一个常见的板换是RGB->YCoCg(参考文献[1112])

Texture

其中,Y是亮度项,Co和Cg是chrominance色度项。逆变换也很简单

Texture

这相当于增加了一小部分。这两个变换是线性的,从方程6.6可以看出,这是一个矩阵向量乘法,它本身是线性的(见方程4.1和4.2)。这一点很重要,因此不必在纹理中存储RGB,而是存储YCoCg,纹理硬件依然可以在YCoCg空间执行过滤,然后PS中可以根据需要转换回RGB。应该注意的是,这种转换本身就是有损的。

还有另外一个可逆的RGB-YCoCg转换,公式如下

Texture

上述公式可以随意的来回变换。例如,24位RGB和相应的YCoCg的转换没有任何损失。需要注意的是,如果RGB的每个分量都是n位,那么Co和Cg需要有n+1位,才能保证可逆转换,Y只需要n位。Van Waveren和Castano(参考文献[1852])使用有损YCoCg变换在CPU和GPU上实现对DXT5/BC3的快速压缩。它们将Y存储在alpha通道中(因为它具有最高的精度)。而Co和Cg存储在RGB的前两个分量中。由于Y是单独存储和压缩,所以压缩会变快。对于Co和Cg组件,它们找到一个二维边界框并选择产生最佳结果的框对角线。注意,如果在CPU中动态创建的纹理,最好也在CPU做压缩,反之,如果在GPU上创建的纹理,最好也在GPU做压缩。YCoCg变换和其他亮度色度变换通常被用于图像压缩,其中色度分量在2*2的像素上被平均,这样可以减少50%的存储空间,而且由于色度变化缓慢,因此看起来还不错。Lee Steere和Harmon(参考文献[1015])进一步将其转换为HSV(Hue-saturation-value色调饱和度值)。将x和y中的色调和饱和度降低4倍,存储在单通道DXT1纹理。Van Waveren和Castano也描述了压缩normal map的快速方法(参考文献[1853])

Griffin和Olano(参考文献[601])的一项研究表明,将多个纹理应用于一个使用复杂着色模型的几何模型上的时候,降低纹理的质量,而不会产生任何感知上的差异。因此,根据不同的例子,质量的降低是可以被接受的。Fauconneau(参考文献[463])提出了一种基于DX11 SIMD实现的纹理压缩格式。

6.3 程序纹理

通过纹理坐标,然后从图像中进行采样,是生成纹理值的一种方法。还有另外一种方法是通过函数,计算得到一个程序纹理。

尽管在离线渲染领域,程序纹理非常常见,但是在实时渲染领域,基本还都是在使用图像纹理。这是因为现代GPU中图像纹理硬件的效率非常高,可以在一秒钟执行数十亿次纹理访问。然而,GPU体系结构正朝着计算成本较低和内存访问成本较高(相对而言)的方向发展。这个趋势使得程序纹理在实时应用中得到了更多的应用。

volume texture因为高存储成本,所以非常适合使用程序纹理。这种纹理可以通过多种技术合成。最常见的方法之一就是使用一个或者多个噪声函数生成(参考文献[407、1370、1371、1372])。如图6.24所示。噪声函数通常连续的以频率的二次方进行采样,被称为octaves倍频率。每个octave都会有一个权重,频率越高权重越低,这样加权和被称为turbulence湍流函数

Texture

上图为程序实时生成的volume texture。左侧的大理石使用ray marching生成的半透明volume texture。右侧,为一个合成图像,由一个复杂的木材材质程序生成(参考文献[1054]),并合成在真实环境上。(左图来自shadertoy的《playing marble》,由stephane guillitte提供,右图由Nicolas Savva提供,来自Autodesk公司)

由于噪声计算的成本问题,一般都是通过与计算得到三维数组的方式,然后用来插值纹理。有很多方法可以使用颜色缓冲区混合来快生成这些数组(参考文献[1192])。Perlin(参考文献[1373])提出了一种快速、使用的方法来使用这些噪声函数,并给出了一些应用。Olano(参考文献[1319])提出了噪声生成算法,允许在纹理采样和执行计算之间进行权衡。McEwan等人(参考文献[1168])开发了经典噪波,和单纯形噪波一样,不需要任何lookup,就可以在shader中计算,并提供出了源代码。Parberry(参考文献[1353])使用动态变成将计算分摊到几个像素上,以加速噪声计算。Green(参考文献[587])提供了一种更高质量的方法,但是对于交互式应用程序来说消耗太大,因为它使用了50个PS指令来进行计算。Perlin(参考文献[1370、1371、1372])提出的噪声还有改进版本。Cook和DeRose(参考文献[290])提出了wavelet noise小波噪声,在只增加少量成本的情况下避免了锯齿问题。Liu等人(参考文献[1054])使用各种噪声函数来模拟不同的木材纹理和表面光洁度。我们还推荐Lagae等人关于这个话题最新的报告(参考文献[956])

还可以使用其他的一些程序纹理,比如通过测量每个位置到散步在空间中一组特征点的距离,形成cellular texture。以各种方式映射生成的最近距离,比如颜色、着色法线,创造看起来像细胞、石板、蜥蜴皮肤和其他自然纹理的团。Griffiths(参考文献[602])讨论了如何在GPU上有效的找到最近的邻居,并生成cellura texture。

另一种程序纹理是物理模拟或者其他交互过程(比如water ripple或者裂缝)的结果。在这种情况下,程序纹理可以有效的对动态条件产生无限的变化。

在生成程序化二维纹理的时候,参数化问题可能比绘制纹理更难,因为绘制纹理的时候,拉伸或者接缝都可以手动处理。一个解决方案是通过将纹理直接合成到曲面上来完全避免参数化。在复杂曲面上执行此操作在技术上具有挑战性,这个领域一直很活跃。可以看下Wei等人(参考文献[1861])在这个领域的概述。

程序纹理的抗锯齿处理相比图像纹理来说,又难又容易。一方面,mipmap等预计算技术无法使用,这样给程序员带来了负担。另一方面,由于程序纹理的作者完全可以自我控制文立中的内容,所以可以同构裁剪的方式避免产生锯齿。这个方式对求和多个噪声函数创建的纹理来说尤为如此。因为每个噪声函数的频率都是已知的,那么可以丢弃任何可能导致锯齿的频率,从而在降低计算成本的同时解决锯齿问题。还有多种技术可被用于其他类型的程序纹理进行抗锯齿处理(参考文献[407、605、1392、1512])。Dorn等人(参考文献[371])讨论了上述工作,并提出一些重构纹理函数以避免高频(比如band-limited)的过程。

6.4 纹理动画

物件表面所使用的图像并非一定要是静态的。比如视频源可以被用作从帧到帧变化的纹理。(Patrick:这就是序列帧吧)

纹理坐标也并非必须是静态的。开发者可以显式的逐帧改变纹理坐标,可以在mesh数据本身中进行更改,也可以通过VS或者PS中的函数进行更改。想象一下,建模一个瀑布模型,然后使用一个看起来像落水的图像纹理。假设v为流动方向,要想使得水流动,必须从每个连续帧的v坐标中减去一个量。在纹理坐标中做减法的效果是使得纹理本身看起来像是在向前移动。(Patrick:uv动画)

通过对纹理坐标使用矩阵变化可以创造更精细的效果。除了平移,还可以实现线性变换,比如缩放、旋转、shearing(参考文献[1192、1904])、warping图像扭曲、morphing transform变形(参考文献[1729])、generalized projections广义投影(参考文献[638])。通过在GPU或者shader中使用函数,还可以实现更多更精细的效果。

通过纹理混合技术,还可以实现其他动画效果。比如,将一个雕塑先用上一个大理石纹理,然后慢慢褪色变成肉质纹理,就可以使得雕塑栩栩如生。(参考文献[1215])(Patrick:这个蛮有意思的)

6.5 材质映射

纹理的一个常见用途是修改材质球中的一个属性,来影响着色方程式。现实世界中的对象通常具有不同的表面材质属性。为了模拟这些对象,PS可以在进行着色运算之前,通过纹理修改材质参数。纹理最常用于表面颜色的修改,这个纹理被称为albedo或者diffuse。然而,其实任何参数都可以通过纹理进行修改、替换、倍增或者其他的一些处理方式。例如,在图6.25中,将三种不同的纹理作为参数应用于一个表面。

Texture

上图中的左侧是金属砖和灰泥。右侧是颜色贴图、roughness(较浅的地方较粗糙)和bump map height凹凸贴图高度(较浅较高)贴图。(图片来自three.js示例webgl tonemapping(参考文献[218]))

纹理在材质中的应用可以更进一步。纹理可以用于控制PS中的分支,而非修改着色表达式中的参数。比如,可以将两个或者多个不同的着色方程式和参数用于一个曲面,然后通过纹理指定曲面中的哪个区域使用什么材质,从而执行不同的代码。比如,具有生锈区域的金属材质,可以通过纹理指定生锈的位置,从而分区域的执行计算生锈部分材质,或者光滑部分金属材质(详见9.5.2节)。(Patrick:Mask图)

着色模型的输入部分,比如颜色,与最终颜色输出有线性关系。因此,包含这些输入的纹理可以用标准技术过滤,从而避免了锯齿。而有些纹理包含非线性着色输入,比如粗糙度或者bump map(第6.7节),就需要小心处理以避免锯齿。可以使用着色模型中的过滤算法来改善结果。第9.13节讨论了这些技术。(Patrick:比如高光AA?)

6.6 Alpha Mapping

alpha值可以被用作实现很多效果,比如alpha blend或者alpha test,比如渲染植物、爆炸和distant object等(Patrick:远处的对象为啥要用alpha)。本节将讨论alpha纹理的使用,并指出相关的各种限制和解决方案。

一个相关的效果是贴花。举个例子,假如你想把一朵花的图案放在茶壶上。那么首先,将所有的alpha值设置为0,则会完全透明。正确设置贴花纹理的alpha值,可将贴花与茶壶原貌进行混合。通常,会使用clamp的方式配合透明边界一起使用,将贴花的单个副本应用于曲面(相对比repeat模式)。图6.26显示了如何实现贴花的实例。有关贴花的详细信息,参见20.2节。

Texture

上图为实现贴花的一种方法,首先,先渲染整个场景,然后渲染一个box,然后贴花会投影到box中的顶点。最左边的纹素是完全透明的,右侧的黄色纹素不可见,因为投影到了被挡住的地方。其余部分都正常投影到了物件上。

alpha的另外一个应用是cutout。假如你制作了一个灌木的贴花图像,并将其应用在了场景中的一个矩形上。其原理与贴花相同,只是不需要依附在其他物件上,而是作为一个物件独立存在。通过这种方式,就可以使用一个简单的矩形渲染具有复杂轮廓的对象。

针对上面这个方案,如果你旋转着看这个物件,就会发现效果出错,因为一个面片是没有厚度的。解决方案就是将这个面片复制一份,并沿着树干旋转90度。这两个矩形片就形成了一个廉价的三维灌木,这种做法被称为cross tree十字树(参考文献[1204]),从水平方向观察时,这种方案相当有效。如图6.27。Pelzer(参考文献[1367])提出了一种类似的方案,使用三个片来表示草。在第13.6节中,会讨论一个叫做billboard的方案,该方案将这种渲染方式减少为使用单个面片。但是,如果观察者从上方以俯视角进行观察的时候,又会出现效果错误,见图6.28。为了解决这个问题,可以使用不同的方案来添加更多的面片,以提供更令人信服的模型。第13.6.5节讨论了一种生成此类模型的方法。第857页的图19.31也提出了另外一种方法,关于最终结果的示例,可以参见第2和第1049页的图片。

Texture

上图中的左侧,为灌木贴图以及1位的alpha通道贴图。右侧为渲染着灌木的单个面片,以及旋转90度后的第二个面片,加一起组成了一个廉价的三维灌木。

Texture

上图左侧为水平方向观察cross tree十字树灌木丛,然后右侧为从上俯视观察,效果就错了。

结合alpha贴图和纹理动画,可以产生非常不错的特殊效果,比如闪烁的火炬、植物生长、爆炸和大气效果。

使用alpha map绘制物件有几种选择。Alpha Blend(第5.5节) 允许使用小数的透明度值,这样在对象边缘处有抗锯齿效果、但是alpha blend需要绘制在不透明后,且以从后到前的渲染顺序渲染三角形。而cross tree的两个片没有严格意义上的前后顺序,因为每个片都在另外一个前面。即使能正确排序,这样做效率也非常低。因为一块地上可能会有数以万计的此类物件。每个网格对象可以由多个单独的片组成,明确的对每个片进行排序是非常不切实际的。

这个问题可以由几种解决方案。其中一个就是alpha test,即有条件的丢弃PS中alpha值低于给定阈值的像素。通过纹理的alpha通道获取到alpha值,然后根据用户定义的alpha阈值决定哪些像素被discard。这种方式就不在意三角形的渲染顺序了,因为透明片段会被丢弃。通常,我们会希望丢弃alpha为0的像素,这样还有一个额外的好处,就是节省了对应像素的PS计算和merge操作,以及避免错误的将Z缓冲区刷新(参考文献[394])。对于cutout,我们一般会将alpha阈值调高,比如0.5甚至更高,然后忽略alpha值,而非将其进行混合,这样做可以避免顺序的问题。但是这样会降低质量,因为这样的话alpha值相当于只有0和1。另外一个方案,是对每个模型进行两次传递,第一次是写深度,通过cutout,第二次不写深度,只是处理半透明渲染。(Patrick:原本以为这是说alphatestdepthlonly+zequal,但实际上不是,因为第二次是做半透明渲染,这样做的好处是AA?)

Texture

alpha test还有两个大问题,即放大倍数过大(参考文献[1374])以及缩小倍数过大(参考文献[234、557])。当针对alpha test使用mipmap的时候,如果处理方式不同,效果可能会有问题。图6.29的顶部就显示了一个示例,其中树的叶子比想象中discard的多。举一个例子来说明这个额问题。假如有一个具有四个alpha值的一维纹理,即(0.0, 1.0, 1.0, 0.0)。当时用平均值的时候,下一级的mipmap为(0.5,0.5),然后再下一级为(0.5)。假如使用0.75作为alpha阈值,当访问mipmap级别0的时候,将会丢弃4个像素中的1.5个(Patrick:为什么,难道不是2个么)。但是在访问下面两级的时候,所有内容都会被丢弃,因为0.5小于0.75。还一个例子见图6.30

Texture

上图中的顶部为,未进行任何矫正的mipmap alpha test。底部为,alpha值根据覆盖范围重新缩放的alpha test。(图片由Ignacio Castano提供,来自“The Witness”)

Texture

上图中的顶部为针对树叶的不同mipmap级别使用blend模式,针对更高级别的可见性做缩放。底部为以0.5为阈值做alpha test,显示了对象为什么在更高级别的mipmap的时候像素越来越少。(图片由Ben Golus提供(参考文献[557]))

Castano(参考文献[234])提供了一种在mipmap创建过程中完成的简单结局方案,效果也很好。比如针对mipmap级别k,通过以下公式确定覆盖率k:

Texture

其中nk是mipmap级别k中纹素的数量,a(k,i)是像素i在mipmap级别k的alpha值,at是等式6.9中用户提供的alpha阈值。这里,假设a(k,i)>at,则结果为1,否则为0。请注意,k=0表示最低mipmap级别,即原始图像。对于每个mipmap级别,可以找到一个新的mipmap阈值ak,而不使用at,这样使得ck等于或者尽可能接近c0。最后,mipmap级别k中的所有纹素的alpha值将乘以ak/at。此方法在图6.29的底部使用。NVIDIA的纹理工具支持此方法。Golus(参考文献[557])给出另外一个方案,可以不修改mipmap的变体,而是随着mipmap级别的增加,alpha在shader中被放大。

Wyman和McGuire(参考文献[1933])提出了另外一个方案,将公式6.9替换成了

Texture

该随机函数返回[0,1],也就是说,平均来说,这将得到一个正确的结果。例如,如果alpha为0.3,则有30%的概率被丢弃。这是一种随机透明的形式,每个像素只有一次机会(参考文献[423])。在实际中,随机函数被替换成了hash函数以避免temporal和高频噪声:

Texture

上述函数也可以变成三维形式,即float hash3D(x,y,z) {return hash2D(hash2D(x,y),z);},返回一个数值[0,1)。hash的输入为模型空间坐标除以模型空间坐标的最大值,然后再进行clamp。需要进一步小心以获得z方向运动的稳定性,这种方案最好与TAA结合使用。这项技术随着距离的推移而逐渐消失,因此近距离观察我们根本不会得到任何随机效果。这种方法的优点是平均每个像素都是正确的,而Castano的方法(参考文献[234])为每个mipmap级别创造一个ak。但是此值可能在每个mipmap级别都不同,而可能降低质量,并需要美术干预。

Alpha test现在放大倍数的时候会显示ripple artifacts伪影,这个问题可以通过将alpha map根据距离预计算来避免(参考文献[234])(详见677页)

将alpha转为覆盖率,以及类似的技术 transparency adaptive antialiasing,将片段的透明度值,转换为像素内覆盖的样本数(参考文献[1250])。这个技术就好比第5.5节中的screen-door transparency一样,但是在subpixel级别的。假设每个像素有4个采样位置,一个片段覆盖一个像素,但是由于cutout的原因,有25%是透明的(75%不透明)。alpha to coverage模式,使得片段变得完全不透明,但它只覆盖四个样本中的三个。此模式对于重叠的草状叶的cutout纹理非常有用,(参考文献[887、1876])。因为每个样本绘制是完全不透明的,距离最近的叶子会沿着边缘挡住后面的对象。由于alpha blend已经关闭了额,所以无需排序,即可正确混合半透明边缘像素。

alpha to coverage有助于alpha test 抗锯齿,但当发生alpha blend的时候可能会出现伪影。例如,具有相同alpha 覆盖率的两个alpha-blend 片段,使用相同的子像素模式,这就意味着一个片段将完全覆盖另一个片段,而不是与其混合。Golus(参考文献[557])讨论了如何使用fwidth()这个shader指令给内容一个更清晰的边。见图6.31。

Texture

使用不同的渲染技术绘制部分alpha覆盖的叶子纹理。从左到右:alpha test、alpha blend、alpha to coverage、alpha to coverage with sharpened edges(图片由Ben Golus提供(参考文献[557]))

使用alpha map之前,需要了解双线性插值作用在颜色值上的工作原理。假设有两个相邻的纹素:一个是纯红色(255,0,0,255),另外一个是几乎完全透明的黑色(0,0,0,2)。两个纹素中间位置的rgba是多少,简单插值得到的结果是(127,0,0,128),是一个比较暗的红色。然而,实际结果并没有变暗,它是一个被alpha预乘的完全的红色。如果插值alpha,为了正确插值,需要确保插值前的alpha已经预乘了要插值的颜色。举个例子,假如另一个颜色是一种微绿色(0,255,0,2)。如果不进行alpha预乘,插值结果会变成(127,127,0,128),这样的话绿色的微小色调突然将结果转成了黄色。而如果使用alpha预乘的话,这个颜色位(0,2,0,2),插值后的结果为(127,1,0,128)。这样的结果更有意义。(Patrick:这个蛮有意思的,所以插值之前要alpha预乘,否则半透明物件的颜色边缘就会出现奇怪的颜色了,这个理论会被GPU中采样纹理的时候时候用,以及后效中blur的时候使用)

在双线性插值的时候,如果没有进行预乘,则会导致贴花等cutout对象周围出现黑色边或者奇怪的颜色。较暗的红色会被认为是没有预乘的值,显示为黑色。即使使用alpha test,这种现象还是会存在。最好的策略就是在双线性插值之前进行预乘(参考文献[490、648、1166、1813])。这一点对网页很重要,所以WebGL也支持这一点。但是,双线性插值是由GPU driver完成,着色器无法对此进行操作。比如PNG之类的图片是不进行预乘的,因为这样会导致颜色精度下降。所以在使用alpha map的时候,可能会因此导致一个黑边。一个常见的解决方案是对cutout图像进行预处理,使用附近不透明纹理的颜色以及alpha为0,来绘制透明黑色纹理(参考文献[490、685])。所有透明区域通常都要使用这种方式进行手动或者自动设置,这样的话,mipmap级别也可以避免边缘问题(参考文献[295])。需要注意的是,当生成mipmap的时候,也需要进行alpha 预乘(参考文献[1933])

(Patrick:这个厉害了,确实可以和美术同步一下,避免alpha test黑边的问题,不过大部分美术应该都知道)

6.7 Bump Mapping

本节将聊一系列用于实现细节的技术,统称为bump map。这些方法通常都是通过修改逐像素着色来实现的。与普通的纹理相比,它们可以在不增加额外的几何体的情况下,提供一个更为立体的外观。

物体上的细节可以分为三个尺度:覆盖很多像素的宏特征 macro-feature,覆盖几个像素的中观特征meso-feature,和小于一个像素的微观特征micro-feature。这些类别也并非绝对的,因为在动画或者交互过程中,观察者可能会在许多距离上观察同一对象。

宏特征 macro-feature是由顶点和三角形或者其他几何primitive表示。当创建三维角色的时候,四肢和头部通常是在宏尺度下建模的。Micro-geometry被封装在着色模型中,在PS中实现,并使用贴图纹理作为参数。所使用的着色模型模拟表面微观几何体之间的相互作用,例如,有光泽的对象,在显微镜下是平滑的,而漫反射表面在显微镜下是粗糙的。角色的皮肤和衣服使用了不同的材质球,或者至少使用了不同的参数,所以表现出来不同的材质。

介于这两个尺寸之间的其它一切,都属于Meso-geometry。它包含了复杂的细节,所以,无法直接使用三角形来操作,然而,又大到可以让观察者直接可以看到。角色脸上的褶皱,肌肉组织细节,衣服上的褶皱和接缝都属于Meso-geometry。用于实现它的一系列方法被统称为bump map,通过调整PS上的一些参数,可以在保持原始几何体平坦的情况下,使得观察者看到一个小的细节。不同类型的bump map之间的主要区别在于它们如何表示细节特征。比如真实感和细节特征的复杂性。例如,数字艺术家经常会将细节雕刻在模型中,然后通过软件将这些几何元素转换成一个或者多个纹理,比如bump map或者occlusion map。

Blinn在1978年提出了将细节写入纹理的想法(参考文献[160])。他观察到,如果在着色过程中,使用一个轻微扰动的表面法线替代真实法线,表面将会出现一些小尺度的细节。他将描述扰动的数据保存在数组中。

这个方法中的关键是,使用一个纹理来修改表面法线,而非改变表面颜色。几何体的实际法线保持不变,而只是修改了照明方程中使用的法线。此操作没有实际物理意义。正如每个顶点有一个法线,会给人一种错觉,即在一个三角形中的表面是平滑的。而修改每个像素的法线会改变三角形表面本身的感知,而不会修改原始几何体。

对于bump map,法线需要相对一个参考来改变。为此,每个顶点处会储存一个切线空间,来用于计算法线。在物体表面应用法线贴图的时候,顶点上应保存顶点法线、切线和bitangent的向量。bitangent经常会被错误的认为是binormal(参考文献[1025])。

切线和bitangent表示对象空间法线贴图本身的轴,最终目的是将法线和灯光放到一个空间中。如图6.32。

Texture

图中显示了一个球面三角形,其中展示了一些顶点的切空间。球体和圆环这样的形状有一个自然的切空间,如圆环上的经纬线所示。

这三个向量(法线n、切线t、bitangent b)构成了基矩阵:

Texture

这个矩阵,有时被缩写为TBN,可以用其将光的方向(针对给定顶点)从世界空间变换到切线空间。这些向量不需要真正的互相垂直,因为法线贴图本身可能会扭曲以适合物体表面。然而,非正交基引入了对纹理的倾斜,这可能意味着需要更多的存储空间,也可能会影响性能,即矩阵将无法通过简单的转置来反转(参考文献[494])。一种节省内存的方法是只存储顶点位置的切线和bitangent,并取它们的叉积来计算法线。然而,只有当矩阵的handedness左右手坐标系始终相同的时候,这个技术才有用(参考文献[1226])。通常,模型是对称的:飞机、人、文件柜或者其他对象。由于纹理消耗大量的内存,所以通常在对称模型上讲使用镜像。因此,只存储对象纹理的一侧,然后通过纹理映射将其应用到模型的两侧。在这种情况下,切空间在两边是不同的。在这种情况下,将在每个顶点处存储一个额外信息来代表左右手handedness,那么依然可以避免存储法线。这样的话,就可以通过这一位来将切线和bitangent的叉积求反,以产生正确的值。如果切空间是正交的,其实也可以存储为四元数(见第4.3节),这样将更节省空间,并且可以节省每个像素的一些计算(参考文献[494、1114、1154、1381、1639])。理论上可能会微小的损失一些质量,而实践中基本很少发生这种情况。

切空间对其他算法也有重要意义。比如下一章将讨论到,很多着色方程只依赖表面法线,然而,比如brushed拉丝或者天鹅绒这样的材料也需要知道观察者的方向以及基于表面的法线。切空间用于定义表面材质的方向。Lengyel(参考文献[1025])和Mittring(参考文献[1226])提供了关于这个方面的报道。Schuler(参考文献[1584])提出了一种基于PS动态计算切空间的方法,这样就无需在每个顶点存储预计算的切线空间了。Mikkelsen(参考文献[1209])改进了这一技术,导出了一种不需要任何参数化方法,而是使用曲面位置的导数以及height field导数来计算法线。然而,与使用标准切空间映射相比,这种技术可能导致显示的细节少很多,并且可能导致艺术工作流问题。(参考文献[1639])

(Patrick:就是通过DDX DDY计算法线吧)

6.7.1 Blinn’s Methods

Blinn最初的bump map是在纹理的每个纹素中保存两个有符号的数值,bu和bv。这两个值对应沿u和v轴改变法线的量。也就是说,这些纹素值将通过双线性插值的方式进行读取,然后用于缩放垂直于法线的两个向量。将这两个向量引用到法线上以改变其方向。bu和bv这两个值描述了表面在顶点上的朝向,见图6.3。这种类型的bump map被称为offset vector bump map或者offset map。

Texture

上图的左侧,通过从bump map获取(bu,bv),并在u和v方向修改法线向量n,然后得到n’(尚未归一化)。在右侧,将显示高度场,及其对着色法线的影响。这些法线可以在高度之间进行插值,以获得更平滑的外观。

另外一种表示bump的方法是使用高度场修改物体表面的法线方向。每个单色纹素值代表一个高度,因此,在纹理中,白色是高区域,黑色是低区域。示例见6.34。这是创建或者扫描bump map时的常见格式,Blinn在1978年也提出了这种格式。高度场贴图和第一个方法中一样,用于导出u和v这两个有符号值。它的做法是通过计算相邻列之间的差异,得到u,以及相邻行之间的差异,得到v(参考文献[1567])。它还有一种变体是使用Sobel滤波器,它可以赋予给邻居更大的权重值(参考文献[535])

Texture

上图为波状高度bump map,以及其在球体上的应用

6.7.2 Normal Mapping

bump map的一个常见形式就是直接存储法线贴图。算法和结果在数学上与Blinn的方法相同,只有存储格式和PS的计算会有点不一样。

法线贴图会将(x,y,z)映射到[-1,1]。比如,对于8位纹理,x轴值0表示-1,255表示1。示例如图6.35所示。浅蓝色[128,128,255],解码后代表的法线值为[0,0,1]。

(Patrick:嗯,这里要划重点) Texture

上图为使用法线贴图来进行bump map。每个颜色通道实际上是一个曲面法线。r通道为x方向,红色越深,法线就越指向右侧。绿色通道为y方向,蓝色通道为z方向。右边为使用法线贴图后的图像。注意看,立方体顶部其实是扁平的。(图片由Manuel M.Oliveira和FabioPolicarpo提供)

(Patrick:想要不是扁平的,还是需要视差贴图)

最初的法线贴图是位于世界空间的(参考文献[274、891]),而在实践中很少这么使用。对于这种法线贴图,用起来很简单,在每个像素的地方,从贴图中检索法线并直接使用它和光照的方向一起计算即可。法线贴图也可以被定义在模型空间,这样模型即使旋转,法线依然有效。但是,无论是世界空间还是模型空间,都将纹理与特定方向的特定几何体绑定了,限制了纹理的重用。

取而代之的,法线贴图通常是在切线空间,相对于曲面上的每个点本身。这允许曲面变形,以最大限度的重用纹理。切空间法线也很容易压缩,因为z分量(也就是未受干扰时的表面法线方向)的符号通常可以认为是正。

(Patrick:嗯,切空间主要就是这三个优势,重用,允许变形,方便压缩)

normal map可被用于提高真实感,见图6.36。

在一个类似游戏场景中使用法线贴图的示例。左上:没有使用法线贴图,左下:使用了法线贴图,右图:法线贴图。(三维模型和normal map由Dulce Isis Segarra Lopez提供)

与filtering 颜色纹理相比,filtering normal map是一个困难的事情。通常,法线和着色颜色之间的关系是非线性的,因此标准的过滤算法可能会出现锯齿。想象一下,当观察闪亮白色大理石砌成的楼梯时。在某些角度,楼梯的顶部或者侧面捕捉光线并反射镜面高光。但是,楼梯的平均法线是45度,(Patrick:这里应该是想说楼梯转角处吧),它将从原始楼梯完全不同的方向捕捉高光。当具有尖锐镜面高光的bump map在不正确的filtering下被渲染的时候,高光就会在特定位置闪烁。

Lambertian中,法线贴图对着色具有几乎线性的效果。Lambertian着色几乎完全就是一个点积,这是一个线性操作。对一组法线求平均值,并对结果执行点积,等于对单个点点积,然后再求平均值。

Texture

需要注意的是,公式左侧的平均向量在使用前未进行归一化。公式6.14表明,不管是过滤还是mipmap,在Lambertian上几乎都能得得到正确结果。但是,最终结果却并不太正确,因为Lambertian着色方程不只是点积,还包含了一个clamp操作,max(ln,0)。clamp操作使得结果非线性,这会使得部分表面变的过暗,然而实际上这样关系不大(参考文献[891])。需要注意的是,一些通常用于法线贴图的纹理压缩方法(比如从其他两个纹理通道重构Z通道),并不支持非单位长度的法线,因此如果使用非归一化的法线贴图可能会对压缩带来困难。

对于非Lambertian表面来说,最好是将着色公式的输入作为一组来进行过滤,而非孤立的过滤法线贴图,这样将产生更好的结果。第9.13节会讨论这种技术。

最终,还可以通过高度图生成法线贴图。具体操作如下(参考文献[405])

Texture

纹素(x,y)处的非归一化法线为:

Texture

通过这种方式的话,纹理边缘处一定要特别注意。

Horizon mapping地平线贴图(参考文献[1027])可用于进一步增强法线贴图,使bump能将阴影投射到它们自己的曲面上。这是通过附加纹理来进行预计算完成的,每个纹理都与曲面平面上的一个方向相关联,并存储每个纹理在该方向的地平线角度。更多信息见11.4节。

6.8 视差贴图

使用normal map来作为bump map的一个问题是,它永远不会随着视角移动位置,也不会相互遮挡。例如,你沿着一堵真正的砖墙看,在某个角度,你将看不到砖墙之间的灰浆。然而使用bump map则永远不会显示这种类型的遮挡,因为它只会改变法线。所以,最好让bump map实际影响每个像素的实际位置。

2001年Kaneko(参考文献[851])提出了parallax map视差映射的概念,Welsh(参考文献[1866])对其进行了改进和推广。视差是指当观察者移动的时候,物体的位置相对的移动。当观察者移动的时候,凸起的部分应该看起来是有高度的。视差映射的关键思想是通过检测被发现可见东西的高度,来猜测其是否真的可见。

视差贴图保存的是heightfield texture高度场纹理。当从某个像素观察曲面的时候,将在该像素检索heightfield值,然后用于移动纹理坐标以检索物体表面上的不同部分。要移动的量取决于检索到的高度以及从眼睛出发的观察角度。如图6.37。heightfield值要么存储在单独的纹理中,要么打包在其他纹理的未使用通道或者alpha通道中(打包不相关的纹理的时候需要小心,因为这可能会对压缩质量产生负面影响)。在使用heightfield值进行坐标偏移之前,需先对其进行缩放和bias偏移。缩放比例决定了heightfield在地表之上或者之下延伸的高度,bias给出了海平面的高度,这个高度上的像素不会进行偏移。下面,给定一个纹理坐标p,调整后的heightfield值h,以及具有高度值Vz以及水平分量Vxy的归一化视图向量V,新的视差调整纹理坐标Padj为:

Texture

需要注意的是,与大多数着色公式不同,这里执行计算的空间很重要,view向量需要在切线空间。

Texture

上图中的左侧为最终目标:通过view向量穿透高度场找到曲面上的实际位置。视差映射通过获取矩形上位置的高度并使用它来查找新位置Padj来进行一阶近似。(Welsh改进前(参考文献[1866]))

虽然这是一个简单的近似值,但如果高度变化相对缓慢(参考文献[1171])。相邻纹素具有相同的高度值,因此使用原始位置的高度值,作为新位置的高度值是合理的。然而这种方法在交钱的视角下是失败的。当view向量靠近曲面的地平线的时候,高度的小变化,就会导致纹理坐标的大偏移。由于检索到的新位置与原始曲面位置几乎没有任何高度方面的关系,所以近似失败。

为了解决这个问题,Welsh(参考文献[1866])引入了限制偏移的思路。这样做的目的是限制了移动量,使其永远不会大于检索到的高度,公式如下:

Texture

注意,这个方程比原来的计算速度快。从几何学上说,高度定义了一个半径,超过这个半径,位置就不能移动。如图6.38所示。

Texture

在parallax offset limiting中,偏移量最多移动原始位置的高度,显示为虚线圆弧。灰色偏移显示原始结果,黑色显示受限后的结果。右边是使用这种技术渲染出来的墙。(图片由Terry Welsh提供)

当垂直物件进行观察的时候,该方程几乎与原始方程相同,因为Vz几乎等于1。而当水平方向观察的时候,两者的效果会不一样。看上去,这样会导致bump变少,然而这比纹理随机采样要好得多。随着视角变化的时候,texture swimming依然会存在一些问题,或者当stereo渲染的时候,观察者同时感知两个视点,这两个视点必须提供一致的深度信息(参考文献[1171])。即使有这样的缺点,使用受限偏移的视差映射值需要花费额外的一些shader指令,即可比基本的normal map,提供了相当大的图像质量改进。Shishkovtsov(参考文献[1631])通过在bump map的法线方向上移动,估计位置,来改善视差遮挡的阴影。

6.8.1 Parallax Occlusion Mapping

bump map不会基于高度场修改纹理坐标,它只会改变某个位置的着色法线。Parallax map提供了高度场效应的简单近似,其假设像素的高度与相邻像素的高度基本相同。然而这种假设很快就被打破。Bump永远不会互相遮挡,也不会投射阴影,我们想要的是在像素处可见的东西,也就是说,view向量首先与高度场相交的地方。

为乐解决这个问题,一些研究人员建议使用ray marching的方法沿着view视角方向查看,知道找到(近似的)交点。此工作可在PS中完成,其中高度数据可以通过纹理访问。我们认为这种研究属于parallax map技术的一个子集,这些技术以ray marching的某种方式完成(参考文献[192、1171、1361、1424、1742、1743])。

这些算法被称为parallax occlusion mapping(POM)或者relief mapping等。其关键思想是,首先沿投影向量测试固定数量的高度纹理样本。如果是水平视角的话,会使用更多的采样,以便不会错过最近的交点(参考文献[1742、1743])。沿着光线的每个三维位置都会被查询,先转成纹理坐标,然后进行采样,确定当前采样点是否位于高度场的上方或下方。一旦样本出现在高度场下方,它以及上一个点在高度场中的坐标,就会被用于寻找交叉点。如图6.39。然后使用法线贴图、颜色贴图等贴图,对该交叉点位置进行着色。multiple layered heightfield可被用户生成overhangs、independent overlapping surface、以及two-sided reliefmapped impostors,见第13.7节。高度场追踪方法也可以用来使得凹凸不平的表面投射自阴影,包括软阴影(参考文献[1742、1743])和硬阴影(参考文献[1171、1424])。见图6.40。

Texture

从上图可以看到,从眼睛发射一条射线去观察平面,然后以固定间隔(紫点)对其进行采样,获取高度。然后以一个近似曲面高度场的黑色线段来确定眼睛看到的第一个交点。

Texture

上图中的左侧,为parallax mapping,但是没有使用ray marching,右侧使用了ray marching。当没有使用ray marching的时候,立方体的顶部就会变平。当使用ray marching的时候,也会产生自阴影的效果。(图片由Manuel M. Oliveira 和Fabio Policarpo提供)

虽然并非全部原创,但还是希望转载请注明出处:电子设备中的画家|王烁 于 2020 年 2 月 12 日发表,原文链接(http://geekfaner.com/shineengine/Translation6_RealTime_Rendering_4th_Edition1.html)