《Real-Time Rendering 4th Edition》读书笔记--简单粗糙翻译 第六章 纹理 Texturing

写在前面的话:因为英语不好,所以看得慢,所以还不如索性按自己的理解简单粗糙翻译一遍,就当是自己的读书笔记了。不对之处甚多,以后理解深刻了,英语好了再回来修改。相信花在本书上的时间和精力是值得的。

———————————————————————————————

“渲染一张图像你要做的是让它看起来是对的。”

   一个表面的纹理就是你看到的和感觉到的画面,想象一副油画的表面。在计算机图形中,纹理(Texturing)就是一个先取得一个表面然后运用一些图片、函数或其他的数据源来修改它在每个位置的表现的过程。举个例子,不需要提供砖墙的一个精准几何模型,仅需要一张砖墙的图像,铺到一个四边形上。除非观察者靠近墙壁,几何细节上的缺陷是看不太出来的。

        然而,仅仅靠着砖墙的表面贴图是不足以令人信服的,因为缺乏几何细节。例如,墙上水泥部分应该是哑光的,砖块区域是光滑的,而在这个例子中观察者会注意到两张材质的粗糙度是一样的。这时候就需要第二张图像纹理了。不像第一张纹理是改变表面的颜色,第二张纹理则会改变表面的粗糙度,这取决于表面的位置。在这个例子中,水泥和砖块从第一张纹理中拿到了颜色,从第二张纹理中拿到了粗糙度。

        问题又来了,观察者发现所有的砖块看起来都是平坦的,这是不对的,砖块表面一般都是不平整的,这时候就可以用到凹凸贴图(bump mapping)。通过改变渲染砖块时的法线,让它们看起来不那么顺滑。如果以一个较浅的角度来观察,问题又出现了,按道理砖块相较于水泥部分会突出,甚至以直线观察过去应该会看到砖块在水泥部分产生阴影。视差贴图(Parallax mapping)解决了这个问题,视差遮蔽贴图(parallax occlusion mapping)用来提高了真实度。置换贴图(Displacement mapping)则是真实的将模型的顶点进行了偏移以产生凹凸感。图6.1是一个用到了颜色纹理和凹凸贴图的例子。

图6.1 纹理。 颜色和凹凸贴图被应用到鱼模型上来增强它的视觉细节等级。

6.1 纹理管线(Texture Pipeline)

        纹理(Texturing)是一种高效修改表面材质和模型的技术。想象一下对单个着色像素所做的事,通过修改在着色方程中用到的数值来改变材质的颜色、光照等等。这种修改的值通常都是基于表面的位置的。以砖墙例子为例,表面上的任意一点的颜色都对应在一张砖墙的图像中,基于表面位置一一对应。在图像纹理中的像素通常称为纹素(texel),用来区分在屏幕上的像素。

        纹理可以由纹理管线来描述。纹理过程的起始点是空间位置,这个位置可以是世界空间,但大部分时候是模型的局部空间,这样模型移动,纹理会跟着一起移动。按Kershaw的话讲,这个空间中的点会经过投影函数,然后得到一组数据,称为纹理座标(texture coordinate),用来访问纹理。这个过程称为映射(mapping),全称是纹理映射(texture mapping)

        在使用纹理座标前,会经过一个或多个对应函数(corresponder functions)把纹理座标转换到纹理空间(texture space),然后通过纹理空间的座标位置(texture space location)来获取纹理对应的值(obtain value),例如,从图像纹理中得到一组索引值来检索像素,检索得到值(texture value)经过转换函数(value transform function)的时候可能会发生变化,最终的结果值(transformed texture value)会用来修改表面的一些属性,例如材质或着色法线。图6.2展示了单个纹理的纹理管线过程。

图6.2 单个纹理的纹理管线过程。

        图6.3就是一个砖墙例子使用这个管线的示意图。找到模型空间下的座标位置(x,y,z),也就是(-2.3,7.1,88.2),然后对这个座标位置进行投影,把(x,y,z)转换成二维座标(u,v),墙上任意一点都会对应一组范围在0 和1之间的纹理座标,例子中是(0.32,0.29)。然后通过纹理座标访问纹理获得对应位置上的颜色值,这张砖墙纹理的大小是256x256,所以对应函数(corresponder function)会把uv座标乘以256,得到(81.92,74.24)。去掉小数点,像素(81,74)找到了对应的颜色值(0.9,0.8,0.7)。这个纹理颜色是在sRGB颜色空间的,如果想要在着色方程中用这个颜色值,需要把它转换到线性空间(0.787,0.604,0.448)。

图 6.3 用砖墙例子展示管线。

6.1.1 投影函数(The Projector Function)

        纹理过程的第一步是要获得表面的座标位置,然后将其投影到纹理座标空间,通常就是二维uv空间。给模型打包的时候通常是允许艺术家给每个顶点定义uv座标的,模型包初始化可能采用的是投影函数(projector function)或者网格展开算法(mesh unwrapping algorithm),艺术家也可以用这种方式改变每个顶点位置的时候改变uv座标。图6.3展示了各种纹理投影。

图 6.4 不同类型的纹理投影。上排从左到右依次是球面的(Spherical)投影、圆柱体的(cylindrical)投影、平面的(planar)投影和natural(u,v)投影。而下排是各投影应用到物体上的表现。

        表面法线也可以是投影函数的输入参数,可用来选择在6个平面投影方向上选取哪一个方向进行投影,通常会在个面衔接缝隙处出现问题,Tarini等人提出了polycube map,模型会被映射到一组立方体投影,不同的体积空间会映射到不同的立方体投影上

        通常一个投影函数足够搞定一个完整的模型,但艺术家们通常会使用工具把模型细分开来,然后给各个部位采用不同的纹理投影方式。如图6.5所示。

图 6.5 展示了如何给一个模型使用多个纹理投影。Box mapping 包含6个平面映射(planar mapping),每一个面对应一个。

        在实时渲染中 ,投影操作通常在建模阶段完成,然后把投影后的结果存在顶点上,当然也不是一直这样,有时候在顶点着色或像素着色中也会用到投影。一些渲染方法,例如环境映射(environment mapping),在计算像素的时候使用了自己特别的投影函数。

        球面投影(spherical projection)将点投射到以某个点为中心的假象球面上

        圆柱投影在计算u纹理座标和球面投影一样,而在计算v纹理座标的时候是按沿着圆柱轴线的距离。这种投影对有着自然轴物体非常有用,例如旋转表面的时候。

        平面投影就像x射线束,沿着一个方向进行平行投影,将纹理应用到所有表面上,它使用的是正交投影。这种投影方式对贴花非常有用。

        在投影方向的边缘通常会有些失真,艺术家们通常需要手动把这些模型分成多个接近平面的部分,也有工具来展开模型网格。图6.6 展示了图6.5中所示的创建雕塑的工作空间。网格展开过程也是很大的研究领域,网格参数化(mesh parameterization)

图 6.6 雕塑模型的一些小纹理合成两张大纹理。右图展示了展开的三角形网格及其如何在纹理中展示。

        纹理座标空间并不总是一个二维平面,有时候是一个三维空间。纹理座标可用三维向量(u,v,w)来表示,其实w是沿着投影方向的深度。有些系统中采用4位座标,通常表示为(s, t, r, q),其中q表示这四个值在一个齐次座标系中,

        纹理座标空间一个重要类型是方向型的,空间中的每个点都可由输入方向来访问。对这个空间可视化的一个方式是在单位球上的点,每个点的法线表示就是在当前位置访问纹理的方向。使用方向参数化的最常见纹理类似就是立方体贴图(cube map)

        因为表面可以应用多个纹理,则可能需要定义多组纹理座标。但是这些座标后面的原理都是一样的:这些纹理座标都是在表面上进行插值得到,用于检索纹理值。在插值之前,这些纹理座标需要由对应函数进行变换。

6.1.2 对应函数(The Corresponder Function)

        对应函数把纹理座标转换到纹理空间座标位置,为纹理应用到表面提供了灵活性。例如,运用API来选择显示一个纹理的某一部分,只有子纹理会在后续操作中应用到。

        对应函数的另外一种形式就是矩阵变换,可以应用在顶点着色或像素着色中。包含平移、旋转、缩放、剪切或投影纹理到表面。需要注意,对纹理变换的顺序和我们期待的顺序相反,因为纹理变换实际影响的是空间。纹理本身是没有进行变换的,而决定了纹理位置的空间被改变了。

        另一类对应函数是控制一个纹理的应用方式。纹理在表面的uv座标范围是[0,1]。但是如果超出了这个范围呢?对应函数决定了该如何表现。在OpenGL中,这类型的对应函数称为“循环模式”(wrapping mode),在DirectX中,这个称为“纹理寻址模式”(texture addressing mode)。常用类型如下:

        ·wrap(DirectX),repeat(OpenGL),或者tile——纹理会重复自己;算法上,纹理座标的整数部分被删掉了。这对重复一张纹理覆盖表面非常有用,这通常是默认的。

        ·mirror——纹理会重复自己,但是每一次重复都是镜像的。例如,一张图像正常表现是从0到1,在1到2则是翻转的,从2到3又回到了正常的,然后再翻转,等等。这位纹理边缘提供了连续性。

        ·clamp(DirectX)或clamp to edge(OpenGL)——超出了[0,1]范围外的值会被钳在这个范围内。这会导致图像纹理的边缘是重复的。当在纹理边缘附加进行双线性插值时,这会对避免采样到纹理相反边缘上非常有用。

        ·border(DirectX)或clamp to border(OpenGL)——超过[0,1]范围外的会使用一个单独定义的边框颜色进行渲染。如图6.7所示。纹理的每个轴可以使用不一样的对应函数,例如,纹理可以在u轴上使用repeat模式,在v轴上使用clamp模式。在DirectX中还一种 mirror once模式,会沿着纹理座标的0值处镜像一次,然后进行clamp,这对对称图案非常有用。

图 6.7 从左到右依次是图像纹理的repeat模式,mirror模式,clamp模式和 border模式。

6.1.3 纹理值(Texture values)

        经过对应函数后会生成纹理空间座标,然后利用座标拿取纹理值。对图形纹理来说,可以访问纹理来检索图像中的纹素信息。在实时渲染中大部分用到的都是图像纹理(Image Texturing),但是也有程序纹理(Procedural Texturing),在程序纹理中,从纹理空间获取纹理值并不是涉及到内存查找,而是函数计算。

        最为直接的纹理值是RGB三个值,用来替换或修改表面的颜色,类似的,还有单一灰度值。另外一种类型是RGBa值,a(alpha)值通常表示的颜色的不透明度,决定了颜色对像素的影响程度。也就是说,也可以存储其他类型的值,比如表面的粗糙度。

6.2 图像纹理(Image Texturing)

        图形纹理中,二维图像是最有效粘贴到三角形表面上的。

        像素着色器访问纹理是通过使用纹理座标调用texture2D。GPU负责把纹理座标转换成像素座标。主要两种纹理座标系统:在DirectX纹理中左上角的座标为(0,0),右下角的座标是(1,1);在OpenGL中,(0,0)在纹理左下角,和DirectX的y轴反向。像素的座标是整数的,但我们经常想访问两个像素直接的位置,然后对其进行混合。这带出了一个问题:像素中心的浮点值座标是多少。有两种系统:截断(truncating)和凑整(rounding)Direct9定义了每个中心座标为(0.0,0.0),使用的凑整。这个系统考虑到DirectX的原点是在左上角,所以会有(-0.5,-0.5)。Direct10修改成了和OpenGL一样,纹素中心为(0.5,0.5)使用的是截断,更准确的说是flooring向下取整。像素(5,9)定义的是在u座标上从5.0到6.0,在v座标上是从9.0到10.0。

        需要注意一个术语:依靠纹理读取(dependent texture read)。有两种定义,第一种尤其适合移动设备,当通过texture2D或类似的函数来访问纹理,依靠纹理读取会发生在像素着色器计算纹理座标时(取代了从顶点着色器中传过来的无修改纹理座标),注意,这意味着对传入纹理座标可做任何修改,甚至简答的交换u和v的值。在早期移动GPU上,如OpenGL ES3.0,就不支持依靠纹理读取,纹素数据可以预取到。另外一种定义对早期桌面GPU很重要,依靠纹理读取发生在当一个纹理座标需要依靠先前的纹理座标的时候。例如, 一个纹理可以改变着色法线,通过访问一个立方体贴图轮流改变纹理座标。

        在纹理图像大小通常是个纹素,其中m和n都是非负整数,这些都是2的幂次方(power-of-two,POT)纹理,现代GPU可以处理非2幂次方(non-power-of-two,NPOT)的纹理了,允许生成的图像可以当做纹理。然而,一些老的移动GPU可能不支持NPOT纹理的mipmapping(多级渐远纹理)。图像加速器对纹理大小的上限不一样,DirectX 12允许的最大纹素是

6.2.1 放大(Magnification)

        在图6.8中,一个48x48的纹理被纹理到一个方片上,方片的大小和纹理的大小很接近,底层图像系统需要放大纹理。放大最为常用的过滤技术有最近邻点插值法(nearest neighbor,实际上是盒式滤波)双线性插值(bilinear interpolation)以及三次卷积插值法(cubic convolution)。这些会提高放大的质量。尽管支持三次卷积插值(又称为双三次插值 bicubic interpolation)的硬件还未普及开,但是可以在着色程序中完成。

图6.8 48x48的纹理放大到320x320。作图使用的是最近邻点插值法滤波,每个像素选择的是最近的纹素;中间图使用的是双线性插值滤波,使用的是四个最近纹素的平均值;右图使用的是三次卷积插值滤波,使用的是一个5x5最近纹素的平均值。

        在图6.8中的左图,使用的是最近邻点插值,这种放大技术的一个特点是一些个别纹素会特别明显,这种现象称为像素化。虽然这种方法的质量不高,但是它每个像素只需要一个纹素。

        同样的图采用双线性插值(有时称为线性插值,linear interpolation),效果如图6.8中间图所示。对每个像素,这种滤波会找到四个邻点纹素,然后在像素二维方向上进行线性插值找到一个混合值。这个结果值是模糊的,很多锯齿消失了。

        回到砖墙的例子,我们得到了p点的uv座标(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')。

图 6.9 双线性插值。左图展示了四个方块对应四个纹素,蓝色点是纹素中心。右图展示了由四个纹素中心组成的座标系。

        定义纹理访问函数为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'),所以可以得到双线性插值颜色值为:

直观地说,越靠近采样点的纹素对最终的结果影响越大。    

        一种常见的模糊放大是使用细节纹理(detail texture)。这些纹理表示的是表面细节,从手机上的抓痕到地面上的灌木丛。这些细节作为单独的纹理叠加在放大的纹理上。高频重复使用细节纹理 ,结合低频放大纹理,视觉效果上类似于使用单一高分辨率纹理。

        双线性插值在两个方向上进行了插值。然而,也可以不需要线性插值。例如,一个纹理由棋盘图案中的黑色和白色像素组成。使用双线性插值在纹理中给出不同的灰度值样本,通过重映射,所有灰度值低于0.4认为是黑色,所有灰度值高于0.6的认为是白色,中间的灰色被拉伸来填补空白。这个纹理看起来更像是一个棋盘格了,同时添加一些混合的纹理。如图6.10所示。

图6.10   最近邻点插值,双线性插值,中间部分通过重新映射,使用相同的2×2棋盘纹理。注意,最近邻点采样给出的正方形大小略有不同,因为纹理和图像网格并不完全匹配。

        在图6.8右图中,使用的是一个双三次插值滤波,效果更好,但是花费也更为昂贵。考虑到昂贵的开销,一种类似的技术被提出来,使用光滑曲线进行插值。两种常见的曲线是smoothstep 曲线和quintic 曲线

对smoothstep曲线有:,并且平滑在0和1之间。同样,quintic曲线有类似的特性:。这两种曲线如图6.11所示。

图 6.11 左边s(x)是smoothstep曲线,右边是quintic 曲线q(x).

图 6.12  四种不同方式来放大一个一维纹理。橙色圆圈表示纹素的中心也表示纹素的值(高度)。从左到右依次是:最近邻点插值,线性插值,每对邻点纹素间使用quintic曲线插值、三次卷积插值。

6.2.2 缩小(Minification)

        当一个纹理被缩小后,一些纹素可能会覆盖一个像素,如图6.13所示。为了给每个像素拿到正确的颜色值,需要整合能够影响到像素的纹素。然而很难确定每个纹素对像素的影响是具体是多少,实际上对实时系统是难以做到高效的。

图 6.13 缩小纹理: 通过一排像素单元格查看一个棋盘格纹理方片,能够粗略看出一个像素被多少个纹素影响。

        因为这些限制,针对GPU的一些算法被提出来了。例如,使用最近邻点插值,和放大纹理类似,选择像素网格中心位置能够看到的纹素。这个算法会引起一些走样。如图6.14所示,最上图使用的就是最近邻点插值法。在水平方向上,因为只选取了众多影响像素的纹素中的一个,就会出现锯齿。当观察者移动的时候,这些锯齿表现的更为明显,属于时间走样(temporal aliasing)的一种表现。

        另外一种经常用到的方法是双线性插值,再次和放大纹理类似,对于缩小来说,这个方法只比最近邻点插值好一点,用混合四个纹素取代了只取一个纹素值,当一个纹素的影响超过四个纹素的混合时,这种滤波会快速失效产生走样。也有好的解决方案,如5.4.1节讨论到的,走样问题可以通过采样和滤波技术解决。一个纹理的信号频率取决于屏幕上纹素之间距离有多近。根据奈奎斯特极限(Nyquist limit),我们需要确保纹理信号频率不超过采样频率的一半。例如,一个图像是黑白线交替组成的,纹素都是分开的,则波长是两个纹素的宽度( 黑线到黑线的距离),所以频率是1/2。为了合适的把纹理显示在屏幕上,频率最低必须要有2x1/2,也就是一个像素一个纹理。所以,通常纹理必须要达到一个纹素一个像素才能避免走样。

图6.14 最上图使用的是点采样(最近邻点插值),中间图是mipmapping,最下图使用的是区域求和表(summed area table)。

        为了达到这个目标,除了加大像素的采样频率,还可以减少纹理的频率。为了解决彻底解决走样问题,各种纹理缩小算法被提出来了。

        在这些纹理反走样算法背后的基本思想都是一样的:对纹理进行预处理并创建数据结构,以帮助计算一组纹素对一个像素的影响的快速近似值。对实时渲染系统,这些算法会花费固定的时间和资源,用这种方式,对每个像素进行一个固定数量的采样,并结合计算一组纹素对像素的影响。

多级渐远纹理(Mipmapping)

        最流行的纹理反走样算法是多级渐远纹理计算(mipmapping)。如今可以在所有图形加速器上完成。“Mip”表示的是 multum in parvo,拉丁语,表示在意小空间里的很多东西——这是一个非常棒的名字来表示原始纹理经过不断重复的滤波来生成更小的图像。

        当采用多级渐远纹理最小化滤波(mipmapping minimization filter),原始纹理会生成一组大小越来越小的纹理,在实际使用纹理的时候,从中取出对应大小的纹理,纹理降采样到原区域的四分之一,每个新的纹素通常都是计算原纹理中的四个相邻纹素的平均值得到。这个缩减是递归实现的,直到纹理的一个维度或两个维度只有一个纹素。这个过程如图6.15所示,整个过程的这组纹理通常称为mipmap chain。计算座标d的目的是确定沿着mipmap的金字塔轴在哪里采样。

图6.15  一个mipmap从原始纹理(0级)开始,在金字塔的底部,每2x2区域会合成下一级的一个纹素。纵轴是纹理的第三个座标,d。在该图中,d不是线性的; 它是一种度量,一个样本使用哪两个纹理层次进行插值。

        生成高质量的mipmap有两点很重要:好的滤波和伽马校正。mipmap通常都是取一个2x2的纹素的平均值得到下一等级的值。使用的滤波是一个盒式滤波,最差的滤波之一,生成的质量比较差,并会造成一些不需要的低频模糊效果,在保持一些高频的时候会引起走样。最好使用Gaussian、Lanczos、Kaiser或类似的滤波。在纹理边缘需要注意使用的是纹理重复使用还是单一拷贝。

        对于在非线性空间中编码的纹理(如大多数彩色纹理),在过滤时忽略gamma校正将修改mipmap级别的感知亮度。 当你离物体越远,使用未校正的mipmaps,物体整体看起来就越暗,对比度和细节也会受到影响。 因此,将这些纹理从sRGB转换为线性空间是很重要的,在该空间中执行所有的mipmap过滤,并把最终结果转回到sRGB颜色空间存储。大部分API是支持sRGB纹理的,所以需要在线性空间生成正确的mipmaps,但是最终结果要保存在sRGB空间。当访问sRGB纹理,首先需要把值转换到线性空间,然后再进行合适的放大或缩小操作。

        当屏幕像素区域投影到纹理上(图6.16),需要一个或多个纹素。使用像素单元的边界严格上是不对的,但是可以用来简单示意下。在单元格外面的纹素是可以影响到当前像素的颜色的,见5.4.1。为了检测出大致有多少纹理会影响到当前像素,常用计算d在OpenGL中称为 λ,又称为 texture level of detail,纹理LOD)的方法有两种。一种是利用像素单元形成的四边形的长边来近似于像素的覆盖范围。另外一种是利用 ∂u/∂x, ∂v/∂x,∂u/∂y,  ∂v/∂y这四个差值的绝对值来作为量度。

图 6.16 左图展示的是一个像素单元方片以及它的纹理视图。右图展示的是像素单元投影在纹理上。

区域求和表(Summed-Area Table)

        区域求和表(SAT)可以避免过度模糊。 要使用此方法,首先要创建一个与纹理大小相同但存储颜色的精度要更高的数组(例如,红绿蓝分别有16位或更高)。 在这个数组的每个位置,必须计算并存储由这个位置和纹素原点(0,0)形成的矩形中所有对应纹理的纹素的总和。在纹理过程中,像素单元在纹理上的投影被绑定在一个矩形中,可以通过访问区域求和表来决定这个矩形的平均颜色,会作为纹理的颜色传递给像素。 使用图6.17所示的矩形的纹理座标计算平均值。计算公式如下:

其中,x和y是矩形的纹素座标,s[x,y]是纹素的区域求和表。 这个方程通过计算从右上角到原点的整个面积的和,然后减去A区域和B区域的面积,C区域的面积被减去了两次,所以需要加回来一次。图6.14展示了使用区域求和表的结果, 接近地平线的线在右边边缘处更清晰, 但是中间的斜线仍然很模糊。 问题是当一个纹理沿着它的对角线被观察时,会形成一个大矩形,许多位于像素附件的纹素会被计算。 例如,想象一个长而薄的矩形,它代表像素单元的反投影,对角横过整个纹理,如图6.17所示。 整个纹理矩形的平均值将被返回,而不仅仅是像素单元内的平均值。

图 6.17 像素单元反投影到纹理上,绑定在一个矩形上;矩形的四个角被用来访问区域求和表。

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

        对于当前的图形硬件,进一步改进纹理滤波最常用的方法是复用现有的mipmap硬件。 其基本思想是将像素单元反投影,然后对纹理上的这个quad进行多次采样,然后对这些采样进行组合。 如上所述,每个mipmap样本都有一个位置和一个与之相关的方形区域。没有使用一个mipmap样本来近似这个quad的覆盖范围,而是使用几个正方形来覆盖这个quad。 quad的短边可以用来确定d(不像在mipmaping中,经常用到的是长边), 这会使每个mipmap样本的平均面积更小,也因此更不模糊。quad的长边用来创建一个各向异性的线,平行于长边,穿过quad的正中间。 各向异性在1:1和2:1之间时, 沿着这条线取两个样本(如图6.18)。 各向异性比例越高,沿轴采集的样品越多。

        这种方案允许各向异性线在任何方向上运行,因此不存在区域求和表的限制, 它也不需要比mipmaps更多的纹理内存,因为它使用mipmap算法进行采样。 图6.19所示就是各向异性滤波的一个例子。

图 6.18  各向异性滤波。 像素单元的反投影创建了一个四边形(quad),在长边间形成了一个各向异性线。

图 6.19 mipmap和各向异性滤波对比。左图是三次线性mipmap,右图是采样16:1比例的各向异性滤波。越靠近水平线, 各向异性滤波提供了更清晰的结果,走样更小。

6.2.3 体纹理(Volume Texture)

        图片纹理的一种直接扩展就是三维图像数据,可由(u,v,w)或(s,t,r)来访问。例如,医学影像数据可以生成为一个三维网格, 通过在这个网格中移动一个多边形,可以查看这些数据的二维切片。一个类似的想法是用这个方式来表示体积光,表面上一个点的光照是通过寻找它在体积内的位置和光照方向来计算的。

        大部分GPU都支持体纹理的mipmapping。 由于在体纹理的单个mipmap层中滤波涉及到三线性插值,因此在mipmap层之间滤波需要进行四线性插值(quadrilinear interpolation)。Sigg和Hadwiger讨论了这个问题及一些涉及到体纹理的其他问题,并给出了一些有效的解决方案,如通过滤波或其他操作。

        尽管体纹理需要更高的内存及高消费的滤波,但是它还是有独立无二的优势。为三维网格寻找一个好的二维参数化的复杂过程可以跳过了,因为三维座标可以直接作为纹理座标了。这避免了二维参数化经常发生的失真和缝合问题。 体纹理也可以被用来表示木头大理石等材质的体积结构。

        利用体纹理对表面进行纹理是极其不高效的,因为绝大多数样本都没用到。Benson、Davis、DeBry等人提出了利用一个稀疏八叉树来存储纹理数据,这个机制非常适合交互式绘画系统, 因为在创建表面的时候不需要显式的分配纹理座标,并且八叉树可以保存任何想要等级的纹理细节。

6.2.4 立方体贴图(Cube Maps)

       立方体贴图(cube map或cube texture)也是一种纹理类型,有六张纹理,分别和立方体的每个面一一对应。通过一个三分量纹理座标向量( 它指定了从立方体中心指向外的射线的方向)来访问立方体贴图。 立方体贴图可用来表示一个方向函数的值;它们最常用于环境映射( environment mapping )。

6.2.5 纹理表示方式(Texture Representation)

        在一个应用中处理很多纹理有几种方法来提高性能。6.2.6节说到的纹理压缩是一种,而本节主要聚焦在纹理贴图集(texture atlases)纹理数组(texture arrays)无约束纹理(bindless texture),这些方法的目的都是来避免在渲染的时候频繁改变纹理的带来的消耗。19.10.1节和19.10.2节讲到了纹理流(texture streaming)纹理编码转换(transcoding)

        为了合批(batch)喂饱GPU,一般尽可能少地改变状态。为了做到这个,可以把一些纹理放入到一张大纹理上,称为纹理图集(texture atlas),如图6.20左图。注意,子纹理的形状是任意的,如图6.6。最优的子纹理布置图集是N¨ oll and Stricker提出的( Efficient Packing of Arbitrarily Shaped Charts for Automatic Texture Atlas Generation )。在生成和访问mipmap的时候仍需要小心,因为mipmap的上层可能包含了一些独立无关的形状。Manson和Schaefer提出了一种优化mipmap生成的算法,通过考虑表面的参数化过程,大幅度改良了结果。Burley和Lacewell提出了一个系统:Ptex,在细分表面的每个quad都有自己的小纹理,这个方法的好处是可以避免在网格上分配独特的纹理座标, 在纹理图集的断开部分的接缝上没有瑕疵。为了通过quad滤波,Ptex使用了adjacency数据结构。

图 6.20 左图:纹理图集,9个小纹理被组合进一个大纹理中。右图:一个更现代的方法是把小纹理组织成一个纹理数组(texture array),在大部分现代API中都能找到这个概念。

        使用图集的一个问题是使用wrapping/repeat和mirror模式,影响的不是子纹理而是整个大纹理另外一个问题是对图集生成mipmap的时候,子纹理会渗透到另外一个子纹理上。 然而,这是可以避免的,在将每个子纹理放入大型纹理图集之前,分别为它们生成mipmap层次结构,并对子纹理使用2的幂次分辨率。

        解决这些问题的一个简单方法是使用texture array API,这会避免mipmapping和repeat模式带来的任何问题,如图6.20右图所示。在纹理数组里的全部子纹理都必须拥有相同的大小,格式,mipmap层次结构,MSAA设置。像纹理图集一样,设置纹理数组只会执行一次,然后在着色可以利用下标访问数组中的任意元素。这会比绑定每个子纹理快5倍。

        还有一个API可以避免状态切换带来的消耗,称为无约束纹理,bindless textures。没有无约束纹理,使用API把一个纹理绑定到特定的纹理,纹理单元的数量上限是一个问题,这加大编程人员的复杂度。驱动需要确保了纹理是保留在GPU上。而有无约束纹理,对纹理数量的上限没有了要求,因为每个纹理都有一个64位指针关联,有时候称为handle,指向纹理数据。这些handle可以有多种方式访问,例如通过uniform,通过varying数据,其他纹理或者一个着色器存储缓冲对象(shader storage buffer object,SSBO)。应用程序需要确保纹理是驻留在GPU端的。无约束纹理避免了驱动的任何类型绑定带来的消耗,使渲染更快。

6.2.6 纹理压缩(Texture Compression)

       固定比例的纹理压缩是一种直接解决内存问题、带宽问题和缓存内容的方法。利用GPU来解码压缩纹理,纹理需要更少的纹理内存,缓存大小变得更高效。同时,这些纹理使用起来更高效,因为访问它们需要而内存带宽更少。通过压缩纹理,可以提供更大的纹理。例如,一个无压缩纹理每个纹素使用3个字节 ,纹理大小是512x512,大小为768kB。使用压缩后,压缩比为6:1,一张1024x1024的纹理只需要512kB。

        压缩算法有很多,文件格式也对应各种各样,例如JPEG,PNG,但是在硬件上进行解码是很费的(19.10.1节会介绍到纹理转码)。S3研发出一个方案,称为S3纹理压缩(S3 Texture Compression,S3TC)被DirectX采样作为标准,称为DXTC(DirectX Texture Compression)。在DirectX 10中被称为BC(Block Compression)。后来,OpenGL的标准实际上也是这个方案,因为计划所有的GPU都支持它。它的优点是创建一个固定大小压缩纹理,有着独立的编码片段,因为解码很简单(也快)。纹理的每个压缩部分都可独立处理。没有依赖查找表或其他依赖,所有解码很简单。

        DXTC/BC的压缩方案有几种,它们有一些共同特性。在4x4的纹素块上进行编码,称为瓦块(tiles)。每块都是独立编码的,编码是基于插值的。对每个编码数量,需要存储两个参考值(例如颜色)。块中每16个纹素的插值因子需要存储起来,沿着两个参考值之间的一个线选择一个值,例如等于两个存储颜色值的颜色值,或对两个存储颜色插值得到的颜色值。 这种压缩只需要存储的两种颜色及每个像素的短索引值。

        7种编码如表6.1所示。注意“DXT”表示是在DirectX9中,“BC”表示在DirectX10和后续版本中。从表中可以看到,BC1有两个16位的颜色值(红色5位,绿色6位,蓝色5位),每个纹素有一个2位插值因子来选择是从两个存储参考值中选一个,还是从两个中间值中选择一个。BC2编码颜色和BC1一样,但是给每个纹素添加了4位(bits per texel,bpt)来量化raw)alpha。对BC3,每个块的RGB数据编码都和DXT1块一样,在这个基础上,alpha数据编码使用了两个8位参考值,以及每个纹素有一个3位的插值因子。每个纹素可以选择两个alpha参考值的一个,或选择6个中间值的一个。BC4只有一个通道,编码和BC3中的alpha编码一样。BC5有两个通道,每一个通道都和BC3中的编码一样。

        BC6H是为高动态范围(high dynamic range,HDR)纹理准备的,每个纹素的RGB三个通道都初始化了一个16位浮点值。这个模式使用了16位,结果是8bpt(bits per texel,bpt)。它有一种模式是针对单行的,另外一种模式是针对两行的。在BC7中,每个块可以有1到3行,存储8bpt。目标是8位RBG纹理、RBGA纹理的高质量纹理压缩。和BC6H有很多相同特性,但是它是针对LDR纹理的格式,BC6H是针对HDR的。注意,BC6H和BC7在OpenGL中对应地被称为BPTC—FLOAT和BPTC。这些压缩技术不只是用于二维纹理,同样也适用于立方体纹理或体纹理。

表 6.1 纹理压缩格式。所有的压缩块为4x4纹素。存储那列表示的是每个块的位数(B)和每个纹素的位数(bits per texel,bpt)。参考颜色的符号前面是颜色通道,后面是每个通道的位数。例如RGB565表示红色是5位,绿是6位,蓝色是5位。

        这些压缩机制的主要确定是会丢失精度(lossy)。以BC1到BC5为例,只有4个或8个插值被用来表示16个像素。如果一个瓦块里有着大量的不同值,会丢失一些精度。实际上,如果正确使用的话,这些机制是可以提供可以接受的保真度。

        BC1-BC5的问题是所有给块用的颜色都是在RGB空间的一条直线上。例如,红色,绿色,蓝色不能表示一个单一块。BC6H和BC7能支持更多行,所以可提供更高的质量。

        对OpenGL ES,有一个压缩算法,称为Ericsson texture compression(ETC),包含在API中。这个机制的效果和S3TC一样,可以快速解码,随机访问,没有直接查找,固定比例。它会把一个4x4纹素压缩进一个64位的块,每个纹素4位。基本原理如图6.21所示。每个2x4块(或4x2块,基于那种效果更好)存储一个基本颜色。每个块也会从一个小的静态查找表中选择四个常量,块中每个纹素可以选择从该表添加其中一个值。这修改了每个像素的光照。纹理的质量和DXTC相当。

图 6.21 ETC(Ericsson Texture Compression)对每个像素块进行编码,然后修改每个像素的光照来创建最终纹素颜色值。

        相比于ETC算法,ETC2,包含在OpenGL ES3.0中,使用了未使用位组合(unused  bit combinations)添加了更多的模式。未使用位组合是也是一种压缩的表示,解压后得到的也是相同的纹理。例如,在BC1中,将两个参考颜色设置为相同是无用的,因为这将表示一个常量颜色块,而只要一个参考颜色包含该常量颜色,就可以获得该常量颜色块。ETC2添加了两种新的模式,使用4个颜色值,最终得到的模式是在RGB空间中的平面,用于处理平滑过渡。Ericsson alpha Compression(EAC)是对纹理alpha值得压缩,这种压缩和基于ETC压缩类似,但是只针对一个组成部分,并且结果纹理每个纹素存储4位。可以选择和ETC2一起。ETC1,ETC2,EAC都是OpenGL4.0的核心,OpenGL ES3.0,Vulkan及Metal的组成部分。

        法线贴图的压缩需要更加小心,通常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平面上的一个方框将法线包围起来,每个4x4块的法线可以使用该方框内的8×8法线(为清晰起见,此处仅显示4×4块法线)。

        在不支持BC5/3Dc或EAC格式的硬件上,一种常见的备用方法是使用dxt5格式纹理,并将两个组件存储在绿色通道和alpha组件中(因为它们的存储精度最高)。其他两个组件未使用。

        PVRTC是应用在被称为PowerVR硬件上的纹理压缩格式,如今广泛应用在iPhones和iPads上。它以4x4纹素为一个块进行压缩,并为每个纹素提供2bit'和4bit两种方案。关键思想是为纹理提供了两个低频(平滑)信号,通过使用相邻纹素数据的块和插值得到。然后在两个信号间每个纹素使用1位或2位进行插值。

        自适应可伸缩纹理压缩(Adaptive scalable  texture compression,ASTC)会将一个n x m个纹素的块压缩进128bit,块大小范围从4x4到12x12,结果也不同,从低至每个纹素0.89 bit到每个纹素8 bit。ASTC使用了很多技巧来实现紧凑的索引表示, 每个块可以选择行数和端点编码。此外, ASTC可以处理任何从1-4通道的纹理以及LDR和HDR纹理。 ASTC是OpenGL ES 3.2及后续版本中的组成部分。

         上述所有的纹理压缩方案都是有损的,而当压缩一个纹理时,在这个过程中花费时间不同。花费几秒甚至几分钟进行压缩,要想获得高质量的压缩, 这通常是作为离线预处理完成的,并存储起来供以后使用。当然,如果你只想花费几毫秒,低质量的结果,纹理压缩可以进行近实时压缩,可立即使用。例如,天空盒每隔一秒左右会重新生成一次,因为云朵会有缓慢移动。因为使用固定函数硬件,解压缩会非常快。压缩数据要比解压花费的时间长很多,这种差异称为数据压缩不对称(data Compression asymmetry)

        Kaplanyan提出来一些方法来提升压缩纹理的质量,包含颜色值纹理和法线贴图, 建议使用每个分量使用16位来创建映射。 对于颜色纹理,然后执行直方图重正化(histogram renormalization)(在这16位上),然后在着色器中使用缩放和偏置常数(bias constant)(每个纹理)来反转其效果。 直方图归一化(histogram normalization)是一种将图像中使用的值扩展到整个范围的技术,可以有效增强对比。 每个分量使用16位可以确保在重正化之后,直方图中没有未使用的槽位,这减少了许多纹理压缩方案可能引入的带宽干扰。如图6.23所示。 此外,Kaplanyan建议,如果75%的像素高于116/255,那么纹理使用线性颜色空间,否则将纹理存储在sRGB中。 对于法线贴图,他还注意到BC5/3Dc经常独立于y压缩x,这意味着并不总是能找到最好的法线。 相反,他建议对法线使用以下误差度量:

其中n是原始法线,nc是经过压缩后又解压的法线。

图 6.23  纹理压缩中,每个分量使用16位而不是8位的效果。从左到右依次是:原始纹理,每个分量8位的DXT1压缩,每个分量16位且在着色器中经过了重正化的DXT1压缩。 纹理已被渲染了强烈的灯光,以更清楚地显示效果。

        需要注意到在不同的颜色空间进行压缩纹理,会加速纹理的压缩。常用的变换是从RGB->YCoCg:

其中Y是亮度,Co和Cg是色度。这个逆变换也不贵:

少量的增加。这两种变换都是线性的, 从这个方程中可以看出6.6是一个矩阵-向量乘法,它是线性的(见方程4.1和4.2)。 这很重要,因为它可以存储YCoCg,而不是在纹理中存储RGB;  纹理硬件仍然可以在YCoCg空间中进行滤波,然后像素着色器可以根据需要转换回RGB。注意,这种转换本身是有损的,这可能有关系,也可能没有关系。

        RGB->YCoCg变换的另外一种可逆的变换总结如下:

其中》表示右位移。这意味着可以两者之间来回转换。 需要注意的是,如果RGB中的每个分量都有n位,那么Co和Cg都有n + 1位,以保可逆转换;Y只需要n位。

6.3 程序纹理(Procedural Texture)

        给定一个纹理空间位置,执行图像查找(image lookup)是生成纹理值的一种方法。 另一种方法是对函数求值,被定义为程序纹理(procedural texture)

        尽管程序贴图纹理通常用于离线渲染应用程序,但图像纹理在实时渲染中更为常见。这是由于在现代gpu中图像纹理硬件的超高效率,它可以在一秒钟内执行数十亿次纹理访问。然而,GPU架构正朝着更便宜的计算和(相对)更昂贵的内存访问的方向发展。这些趋势使得过程纹理在实时应用中得到了更大的应用。

        体纹理是程序纹理的一个非常有魅力的应用。考虑到体图像纹理需要高存储,这些纹理可以通过各种技术生成,最常用的方法是使用一种或多种噪声函数来生成。如图6.24所示。 噪声函数通常是在连续的2次幂的频率上采样,称为octaves每个octave给一个权重,通常会随着频率增加而衰减,这些加权采样的和称为turbulence功能

图 6.24 两个使用体纹理的实时生成的程序纹理例子。左边大理石是使用光线行进(ray marching)渲染的半透明体纹理。右图的物体是由在真是环境上使用了复杂的程序木材质着色器的合成图像生成。

        因为计算噪声的花费较大,在三维数组中的格点通常是预先计算的,然后使用在纹理插值上。有许多方法可以使用颜色缓冲混合来快速生成这些数组,例如Perlin,Olano,McEwan,Parberry,Green,Cook和DeRose这些人提出的算法。

         另一种类型的程序纹理是物理模拟或其他交互过程的结果,如水波或扩展的裂缝。 在这种情况下,程序纹理可以有效地在动态条件下产生无限的变化。

         当生成一个二维的程序纹理时, 参数化问题可能比创建纹理带来更多的困难,在创建纹理时,拉伸或缝合导致的瑕疵可以手工修改或修改。 一个解决方案是通过直接在表面合成纹理来完全避免参数化。 在复杂表面操作是一项技术挑战,也是一个活跃的研究领域。

        程序纹理的反走样要比图像纹理的反走样既困难又简单。一方面,预先计算方法,例如mipmapping不可用,给程序员带来负担,另外, 程序纹理构造者拥有关于纹理内容的“内部信息”,因此可以对其进行调整以避免走样。 这对于通过合并多重噪声函数而创建的过程纹理尤其正确。 每个噪声函数的频率是已知的,因此任何可能引起走样的频率都可以丢弃,实际上降低了计算的成本。

6.4 纹理动画(Texture Animation)

        应用在表面的纹理并不一定是静态的(static),例如, 视频源可以用纹理,每帧都在变化。

        纹理座标也不一定是静态的, 无论是在网格的数据本身或通过应用在顶点或像素着色器的函数。 想象一下,建模一个瀑布,它的纹理需要看起来就像落水一样。假设v座标就是水流的方向,为了确保水流动, 每一个连续帧的v座标都需要减去一个分量。 从纹理座标中减去一个值会使纹理本身看起来向前移动。

         更精细的效果可以通过对纹理座标使用一个矩阵变换。 除了平移之外,还允许进行线性转换,如缩放、旋转和剪切,图像扭曲变形变换以及广义投影。 通过使用纹理混合技术,可以实现其他动画效果。例如,从大理石纹理开始,逐渐淡化为肉体纹理,就可以使雕像栩栩如生。

6.5 材质贴图(Material Mapping)

         纹理的一个常见用法是修改材质属性来影响着色方程。 真实世界的物体表面通常有不同的物质属性。 为了模拟这样的物体,像素着色器可以从纹理中读取值,并在计算着色方程之前使用它们修改材质参数。 最常被纹理修改的参数是表面颜色。 这种纹理被称为反射率彩色贴图(albedo color map)漫反射彩色贴图(diffuse color map)。 但是,任何参数都可以通过纹理进行修改:替换它、乘以它或以其他方式更改它。 例如,在图6.25中,三个不同的纹理被应用到一个表面,代替了常量值。

图 6.25 金属砖和砂浆。 右边是表面颜色的纹理,粗糙度(越亮越粗糙),凹凸贴图的高度(越亮越高)。

         纹理在材料中的使用可以更进一步。 不需要修改方程中的参数时,纹理可以用来控制像素着色器本身的流(flow)和功能。 两个或两个以上材质有不同的着色方程和参数时,可以通过一个纹理指定表面的哪些区域有哪些材质来应用到一个表面上,从而导致对每个材质执行不同的代码。 例如,有一些生锈区域的金属表面可以使用一个纹理来指示锈迹的位置,根据纹理查找有条件地执行着色器的生锈部分,否则执行闪亮的金属着色器。

         着色模型的输入,如表面颜色,与着色器输出的最终颜色有线性关系。 因此,包含这些输入的纹理可以用标准技术进行填充,避免走样。 包含非线性着色输入的纹理,例如粗糙度或凹凸贴图(章节6.7),需要更加小心以避免走样。 考虑到着色方程的滤波技术可以改善这些纹理的结果,这些技术将在9.13节讲到。

6.6  Alpha贴图(alpha mapping)

        Alpha值可以用于许多使用alpha混合或alpha测试的效果,例如有效渲染树叶、爆炸和远处物体等等。 本节讨论了alphas中纹理的使用,同时指出了各种限制和解决方案。

         一种与alpha纹理相关的效果是贴花(decaling)。 例如,假如你想在茶壶上画一幅花的图案,你不需要整幅图,只需要花所在的部分。 通过给纹素的alpha赋值0,可以使它透明,这样就不会产生任何影响。 因此,通过适当地设置贴花纹理的alpha值,您可以用贴花替换或混合底层表面。 通常,钳制对应功能(clamp corresponder function)是用一个透明的边框把贴花的一个副本(相对重复的纹理)应用到表面。 图6.26展示了贴花是如何应用的。有关贴花的更多信息,请参见20.2节。

图 6.26 贴花的一种实现。先渲染一次场景得到帧缓冲, 然后渲染一个框(box),对于框内的所有点,贴花纹理被投影到帧缓冲内容中。最左边的纹素是完全透明的,所以它不会影响到帧缓冲,黄色纹素之所有看不见,是因为它被投影到表面隐藏起来的那部分上了。

        Alpha纹理的另一个应用是制作Cutouts。 假设你制作了一个灌木的贴花图像,并将它应用到场景中的一个矩形上。 其原理与贴花相同,不同之处在于,cutout不会与下垫面齐平,cutout是绘制在其背后的任何几何图形的顶部。 通过这种方式,使用单个矩形,您可以呈现具有复杂轮廓的对象。

        在灌木的例子中,如果你旋转观察它,错觉就失败了,因为灌木没有厚度。一种方法是复制这个矩形并沿灌木躯干旋转90度。这两个矩形构成了一个便宜的三维灌木,有时被称为“交叉树”(cross tree)。 这种错觉从地面上看是相当有效的。如图6.27所示。 Pelze提出了一个类似的方案,使用三个cutouts来表示草。在13.6节,将会介绍一种称为广告牌(billboarding)的技术, 用来将这样的渲染减少到一个单一的矩形。如果观察者移动到地面以上,当从上面看到灌木时,错觉就会消失,将会看到两个cutouts。如图6.28所示。 为了解决这个问题,可以通过不同的方式添加更多的cutouts——切片、分支、分层——来提供更有说服力的模型。

图 6.27 左边展示的是灌木的纹理贴图和1bit的alpha通道贴图。右边是灌木渲染在一个矩形中,通过添加第二个(复制第一个)矩形,然后旋转90度,可以形成一个便宜的三维灌木。

图6.28  从离地面稍远一点的地方看“交叉树”灌木,然后离得更远看,发现错觉就消失了。

         结合alpha纹理和纹理动画可以产生令人信服的特殊效果,如闪烁的火炬,植物生长,爆炸,和大气效果。

         有几个选项可以用来渲染带有alpha贴图的对象。Alpha混合(5.5节)允许部分透明值(便于对象边缘反走样),以及部分透明对象。 然而,alpha混合需要不透明三角形渲染完后再去渲染需要混合的三角形,并按从后到前的顺序渲染。一个两个cutout纹理构成的交叉树,没有渲染顺序是正确的,因为每个一个四边形都有一部分在另外一个的前面。例如,一块草地可能有成千上万个由2个cutout表示的草, 每个网格对象可以由许多独立的方片。明确地对每个方片进行分类是非常不切实际的。

         在渲染时,这个问题可以通过几种不同的方式得到改善。 一种是使用alpha测试,有条件地丢弃alpha值低于像素着色器中给定阈值的片段的过程。操作如下:

其中,texture.a是纹理查找的alpha值, 参数alphaThreshold是一个用户提供的阈值,它决定哪些片段将被丢弃。 这个二进制可见性测试允许三角形以任意顺序呈现,因为透明片段会被丢弃。 我们通常希望对任何alpha值为0.0的片段执行此操作。 丢弃完全透明的片段还有一个额外的好处,即节省了进一步的着色器处理和合并的成本,以及避免在z-buffer中错误地将像素标记为可见。 对于cutout,我们通常将阈值设置为0.0以上,比如0.5或更高,然后进一步忽略alpha值,而不是将其用于混合。这样可以避免无序的瑕疵。 但是,质量很低,因为只有两层透明(完全不透明和完全透明)。 另一种解决方案是为每个模型执行两个pass,一个用于固体cutout,它被写入z缓冲区,另一个用于半透明的采样,它不是写入z缓冲区。

         Alpha测试还有另外两个问题,即放大倍数过大和缩小倍数过大。 当alpha测试与mipmapping一起使用时,如果不进行不同的处理,效果可能难以令人信服。如图6.29上图所示那样, 树叶变得比预期的更透明。这可以用一个例子来解释。 假设我们有一个具有四个alpha值的一维纹理,即(0.0,1.0,1.0,0.0)。 通过平均,下一个mipmap级别变成(0.5,0.5),然后最高级别是(0.5)。 现在,假设我们使用αt = 0.75。当访问mipmap level 0时,可以看到每4个纹素将有一个会通过丢弃测试。 但是,当访问接下来两个级别时,由于0.5 < 0.75,所有内容都将被丢弃。

图 6.29 上图: 使用了mipmapping的alpha测试,没有任何修正。下图: 根据覆盖率重新调整了alpha值的alpha测试。

图 6.30  顶部是不同mipmap级别的叶子的混合,较高的级别为了能见度进行了放大。 底部显示是alpha测试值为0.5的不同级别的mipmap,展示了远离物体时像素是如何减少的。

        Castano提出了一个简单的解决方案。对mipmap级别为k,所以平均值ck定义为:

其中,nk是mipmap k级的纹素数量,α(k,i)是像素i在mipmap k级的alpha值。αt是用户为方程6.9提供的阈值。在这里,我们假设 α(k,i) > αt时结果为1,否则为0。注意k=0表示最低级别的mipmap,即原始纹理。 对于每一个级别的mipmap,然后我们找到一个新的mipmap阈值αk,而不是使用αt, 这样ck等于c0(或尽可能接近)。这可以由二进制搜索来完成。最终,在mipmap k级的全部纹素的alpha值被缩放为αt/αk。图6.29下图使用的就是这种方法,NVIDIA的纹理工具有支持这个方法。

         Wyman和McGuire提出了一种不同的解决方案,公式6.9的一行代码在理论上被替换为:

随机函数返回了[0,1]之间的统一值,这会使得平均值是对的。例如,如果纹理查找的alpha值是0.3,片元将会有30%的机率被丢弃。 这是一种随机透明的形式,每个像素只有一个样本。 在实际应用中,为了避免时空高频噪声,将随机函数替换为哈希函数:

对上述函数的嵌套调用形成了一个三维散列,即

返回[0,1)之间的一个值。

         透明至覆盖(alpha to coverage)和类似功能的透明度自适应反走样,获取片元的透明度值,并传递进像素覆盖的采样点。这个想法和5.5节说到的screen-door透明很像,只不过在子像素级别上。 假设每个像素有四个样本位置,一个片元覆盖一个像素,由于cutout纹理,有25%的透明75%的不透明。透明至覆盖模式使片元变得完全不透明,但它只覆盖了四个样本中的三个。 因为每个绘制的样本都是完全不透明的,最靠近的叶片会隐藏在它后面的物体。 正确地混合半透明的边缘像素是不需要排序的,因为alpha混合已经关闭。

        透明至覆盖对alpha测试反走样效果很好,但是在alpha混合的时候会有瑕疵。 例如,两个具有相同alpha覆盖率的alpha混合片元将会使用相同的子像素模式,这意味着一个片元将完全覆盖另一个片元,而不是与它混合。 Golus提出了使用fwidth()着色器指令来给内容提供更清晰的边缘。参见图6.31。

图 6.31 叶子纹理的边缘有部分alpha覆盖,从左到右的渲染技术依次是:alpha测试(alpha test),alpha 混合(alpha blend),透明至覆盖(alpha to coverage),有尖锐边缘的透明至覆盖(alpha to coverage with sharpened edges)。

         任何使用alpha纹理,了解双线性插值是如何影响颜色值的都很重要。假设有两个相邻的纹素:rgba=(255,0,0,255)是一个纯红色,相邻的纹素rgba=(0,0,0,2),黑色,几乎完全透明。那么两个纹素中间位置的rgba会是多少呢?简单的插值得到的是(127,0,0,128),rgb颜色是一个暗红色。 然而,这个结果实际上并不是暗的,它是一个已经预先乘以它的alpha的完全红色。如果你插值alpha值, 为了得到正确的插值,你需要确保被插值的颜色在插值之前已经预乘了alpha。举个例子,假设几乎透明的纹素的rgba设为(0,255,0,2),给它加了一点点绿色。这个颜色没有预乘alpha值,插值后结果变成(127,127,0,128)。 微小的绿色突然把结果变成了一个(预乘的)黄色样本。 这个相邻的纹素的预乘的rgba如果是(0,2,0,2),则给出了(127,1,0,128)的预乘结果。 这个结果更有意义,结果的预乘颜色大多是红色,带有一点难以察觉的绿色。

         忽略双线性插值,会导致在decal和cutout对象周围产生黑边。暗红结果会被接下来的管线当作未预乘的颜色对待,边缘会变黑。这种效果在alpha测试也会看到。 最好的策略是在双线性插值之前进行预乘。 WebGL的API支持这一点,因为合成对于web页面很重要。 然而,双线性插值通常是由GPU执行的,在这个操作完成之前,着色器无法对纹素值进行操作。 图像在PNG等文件格式中没有预乘,因为这样做会失去颜色精度。 当使用alpha纹理时,这两个因素结合在一起会导致默认的黑色边缘。 一种常见的解决方法是对cutout纹理进行预处理,在透明部分涂上“黑色”纹素,一种从附近不透明纹素的颜色衍生出来的颜色。 所有的透明区域通常都需要用这种方式重新绘制, 手工或自动, 因此,mipmap级别也就避免了边缘问题。 同样值得注意的是,在使用alpha值形成mipmaps时,应该使用预乘值

6.7 凹凸纹理(Bump Mapping)

        本节描述了一个大类:小规模细节表示技术,统称为凹凸贴图(bump mapping)。这些方法都是通过修改每个像素的着色方程来实现的。它们比单独的纹理映射更具有三维效果,但是没有添加任何额外的几何图形。

        物体细节可以被分为三个尺度:宏观(macro-features),覆盖很多像素,中观(meso-features),包含一些像素,微观(micro-features),大小小于一个像素。这些类别有些不固定,因为观察者可以在不同的距离观察同一个物体。

        宏观几何表示的是顶点、三角形或其他几何图元。当创建一个三维角色,四肢和头部是典型的宏观模型。微观几何封装在着色模型中, 它通常在像素着色器中实现,并使用纹理映射作为参数。着色模型模拟了表面微观几何的交互,即,有光泽的物体是在微观下是光滑的,漫反射表面在微观下是粗糙的。 角色的皮肤和衣服有不同的材质,因为它们使用不同的着色器,或者至少在这些着色器中有不同的参数。

        中观几何描述的是介于这两者之间的一切。它包含的细节过于复杂,无法有效地使用单独的三角形渲染,但它足够大,可以让观察者在几个像素上区分表面曲率的个别变化。 人物脸上的皱纹,肌肉组织的细节,衣服的褶皱和接缝,都是中观尺度的。 一组统称为凹凸纹理的技术通常用于中观尺度建模。 它们在像素级调整着色参数, 观察者可以感觉到从基础几何结构中产生的微小扰动(有立体感), 它实际上是平坦的。 不同类型凹凸贴图的主要区别在于它们是如何表示细节特征的。 变量包括了写实的等级和细节特征的复杂性。 例如,数字艺术家通常给模型雕刻细节,然后用软件将这些几何元素转换成一个或多个纹理,例如凹凸纹理,或者渐暗纹理(crevice-darkening texture)

         Blinn在1978年提出了在纹理中编码中观尺度细节的想法。他观察到在着色期间,如果我们用一个轻微扰动的表面法线代替真正的法线,表面似乎有小规模的细节。 他将描述扰动表面法线的数据存储到一个数组中。

         关键的思想是,我们不是使用纹理来改变光照方程中的颜色分量,而是使用纹理来修改表面法线。 曲面的几何法线保持不变;只是修改了照明方程中使用的法线。 这个操作没有物理意义;我们改变表面的法线,但表面本身在几何意义上仍保持光滑。 就像每个顶点都有一个法向量会给人一种表面在三角形之间是光滑的错觉,修改每个像素的法向量会改变三角形表面本身的感知,而不需要修改它的几何形状。

         对于凹凸贴图,法线必须相对于某些参照系改变方向。 为此,在每个顶点上存储一个切线座标系(tangent frame),也称为切线空间基(tangent-space basis)。 这个参照系被用来将光转换到一个表面位置空间(surface location’s space)(反之亦然)来计算扰动法线的效果。 对于应用了法线映射的多边形表面,除了顶点法线外,我们还存储了所谓的切线(tangent)双切线向量(bitangent  vectors)双切线向量又被错误的认为是副法线向量(binormal vector)。

         切向量和双切线向量表示法线贴图本身在模型空间中的轴, 因为我们的目的是将光线转换到相对于地图的光线。如图6.32所示。

图 6.32  一个球面三角形和顶点的切线展示。 球体和圆环面的形状有一个自然的切线空间基,就像圆环面上的经纬度线所显示的那样

        法线n,切线t和双切线b,三个向量构成了一个基础矩阵:

这个矩阵有时候缩写为TBN(Tangent Bitangent Normal),将光的方向(对于给定的顶点)从世界空间转换为切线空间。 这些向量不一定相互垂直, 因为法线贴图本身可能被扭曲以适应表面。 然而,非正交基会导致纹理倾斜,这意味着需要更多的存储空间,同时也会影响性能,例如。,那么这个矩阵就不能被一个简单的转置所反向。 一种节省内存的方法是只存储顶点处的切线和双切线,然后取它们的叉乘来计算法线。 然而,只有当矩阵的旋向性总是相同时,这种方法才有效。 模型通常是对称的:飞机、人、文件柜和许多其他对象。 因为纹理消耗大量的内存,所以它们经常被镜像到对称模型上。 因此,只存储对象纹理的一侧,但是纹理映射将其放在模型的两侧。 在这种情况下,切线空间的旋向性在两边是不同的,不能被假设。 在这种情况下,如果在每个顶点上都存储了额外的信息以表明旋向性,那么仍然可以避免存储法线。 如果正切座标系是正交的,也可以将基存储为四元数,这种更节省空间,并且可以节省每像素的计算量。 质量上的小损失是有可能的,尽管在实践中很少见到。

6.7.1 Blinn算法(Blinn's Methods)

         Blinn的原始凹凸纹理算法在纹理的每个纹素上存储两个有符号的值,bu和bv。 这两个值对应的是沿着图像u轴和v轴改变法线的量。 也就是说,这些纹理值,通常是双线性插值的,被用来缩放正交垂直于法线的两个向量。 这两个向量加到法向量来改变方向。 bu和bv两个值描述了曲面在该点的方向。如图6.33所示。这种类型的凹凸贴图纹理被称为偏移矢量凹凸贴图(offset vector bump map)偏移贴图(offset map)

图 6.33 左图,法向量n被从凹凸贴图u和v方向上的值(bu,bv)修改了,得到n'(没有归一化)。右图,展示了高度场及它对着色法线的影响, 这些法线可以代替插值之间的高度,为了得到更平滑的外观。

         另一种表示凹凸贴图的方法是使用高度场(heightfield)来修改表面法线的方向。 每个黑白色纹理值表示一个高度,因此在纹理中,白色表示高区域,黑色表示低区域(或者反之亦然)。如图6.34所示例子。 这是第一次创建或扫描凹凸贴图时使用的一种常见格式,也是在1978年由Blinn提出的。和第一种算法类似,高度场也得到两个有符号的值u和v。 这是通过取相邻列之间的差来得到u的斜率,取相邻行之间的差来得到v的斜率。 一种变体是使用Sobel滤波,它会给予直接相邻的邻居更大的权重。

图 6.34  波状高度场凹凸纹理及其在球面上的应用。

6.7.2 法线贴图(Normal Mapping)

        凹凸贴图的一个常用方法是用来存储一个法线贴图。该算法和结果在数学上和Blinn算法一样,只是存储格式和像素着色计算不同。

        法线贴图会把(x,y,z)编码映射到[-1,1],即对一个8bit纹理x轴的值,0对应-1.0,255对应1.0。图6.35展示了一个例子。颜色值[128, 128, 255],一个淡蓝色,表示一个平坦的表面,即法线为[0,0,1]。

图 6.35 存储了法线贴图的凹凸贴图。每个颜色通道实际上表面的法线座标。红色通道表示x偏差,越红,越多的法线指向右边,绿色表示y偏差,蓝色表示z偏差。右图是使用了法线贴图生成的图像。注意立方体顶部的扁平外观。

        法线贴图最初是作为世界空间法线贴图引入的,在实践中很少用到。对这种类型的贴图,扰动是很直接的, 在每个像素处,从纹理中检索法线并直接使用,沿着光的方向,去计算表面上该点的着色。法线贴图也可以定义在物体空间,这样模型就可以被旋转而法线仍然有效。 但是,世界空间和对象空间表示都将纹理以特定的方向绑定到特定的几何图形上,这限制了纹理的重用。

         相反,扰动法线通常是在切线空间中检索, 即,相对于表面本身。 这允许表面的变形,以及最大限度地重复使用正常纹理。 切线空间法线也可以压缩得很好,因为z分量的符号(与未扰动曲面法线对齐的那个)通常可以假定为正的。

        法线贴图也可以很好地增强写实感,如图6.36。

图 6.36  一个游戏场景中使用的法线贴图凹凸贴图的例子。左上图没有使用右边两个法线贴图。左下图则应用了法线贴图。右图是两个法线贴图。

         相比较于颜色纹理的滤波,法线贴图的滤波是一个难题。通常,法线和着色颜色值之间的关系并不是线性的 ,所以标准的滤波方法可能会导致走样。 想象一下,楼梯是由闪闪发光的白色大理石砌成的。 某些角度,楼梯的顶部或侧面可以捕捉到光线,并反射出明亮的镜面高光。 然而,楼梯的平均正常角度是45度; 它会从与原来楼梯完全不同的方向捕捉高光。 当带有高光的凹凸贴图在没有正确滤波的情况下渲染时,当采样刚好处在高光来回闪烁位置,可能会出现吸引人的闪光效果。

        Lambertian曲面是一种特殊的情况,在这种情况下,法线贴图对着色的影响几乎是线性的。Lambertian着色几乎完全是一个点积,这是一种线性操作。 对一组法线求平均值并对结果执行点积,相当于对单个法线点积结果求平均值:

注意这些平均向量在使用前并没有归一化。公式6.14展示了标准滤波和mipmap几乎可以为Lambertian表面生成正确的结果。结果不是非常准备因为Lambertian着色方程不是一个点积;它是一个钳制点积:max(l · n, 0)。钳制操作使它不线性。 这将使表面过度变暗,以使光的方向发生偏光,但在实践中这通常是可以接受的。 需要注意的是,一些通常用于法线贴图的纹理压缩方法(比如从另外两个中重建z分量)不支持非单位长度的法线,因此使用非归一化的法线贴图可能会造成压缩困难。

         在非Lambertian曲面的情况下,通过将着色方程的输入作为一个整体进行滤波,而不是单独对法线贴图进行滤波,可能会产生更好的结果。

        最后,从高度贴图派生出法线贴图可能很有用,h(x, y)。 首先,在x和y方向上的导数的近似值是用中心差分来计算的:

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

需要注意纹理的边界处。

6.8 视差贴图(Parallax Mapping)

         凹凸和法线贴图的一个问题是凹凸不会随着视角改变位置,也不会互相遮蔽。 例如,如果你沿着真正的砖墙看,在某个角度你不会看到砖与砖之间的灰浆。砖墙的凹凸贴图则永远不会有这种遮挡,因为它只是改变了法线。 最好是让凹凸贴图实际上影响的表面位置是渲染的每个像素。

         视差贴图的概念是由Kaneko在2001年提出的,并由Welsh进行了改进和推广。 视差是指当观察者移动时,物体的位置也发生相对移动。 随着观察者移动时,凹凸应该看起来有高度。 视差映射的关键思想是通过检查可视像素的高度来对像素中应该看到的内容进行有根据的猜测。

         对于视差贴图,凹凸存储在一个高度场纹理中。 当观察表面上一个给定的像素时,在该位置会得到到一个高度场值,并用于偏移纹理座标以得到表面的不同部分。偏移的幅度取决于得到的高度值以及眼睛到表面的角度。如图6.37所示。高度场值要么存储在单独的纹理中,要么打包在别的纹理未用到的颜色通道或者alpha通道中( 在打包不相关的纹理时必须小心,因为这会对压缩质量产生负面影响)。高度场值在被用来转换座标前有被缩放和偏差 ,缩放决定了高度场在表面之上或之下延伸的高度,而偏差给出了“海平面”高度,在这个高度上不会发生偏移。给定一个纹理座标位置P,矫正过的高度场高度为h,一个已经归一化的观察向量V(高度值为Vz,而水平面分量为Vxy)则新的视差矫正纹理座标Padj为:

注意,不像大部分着色方程,在这里执行计算的空间很重要,观察向量需要定义在切线空间。

图 6.37 左图是目标: 表面上的实际位置可由观察向量穿过高度场来确定。 视差贴图做了一个一阶近似,取右图矩形上该位置的高度,然后用它找到一个新的位置Padj。

        通过一个简单的近似, 如果凹凸高度变化相对缓慢,在实践中效果相当好的。 邻近的纹素也差不多相同的高度,所以利用原始位置的高度来评估新位置的高度是合理的。 然而,这种方法在浅视角下就行不通了。 当观察向量接近表面水平线时,一个小的高度变化会导致大的纹理座标偏移,这种近似是失败的。

        为了改善这个问题,Welsh提出了偏移限制的观点, 这样做的目的是限制偏移量,使其永远不会超过能检索到的高度。方程为:

注意,这个方程比上个方程的计算要快。 几何上的解释是,高度定义了一个半径,超过这个半径位置就无法有偏移。如图6.38所示。

图 6.38 视差贴图的偏移限制。偏移量最多达到原始位置的高度的量,如以虚线表示圆弧。灰色箭头表示的是原始方程的结果,黑色箭头表示则是偏移限制后的结果。

6.8.1 视差遮蔽贴图        

         凹凸贴图不修改基于高度场的纹理座标,它只改变位置的着色法线。视差贴图提供一个简单的高度场效果的近似, 假设了一个像素点的高度与相邻像素点的高度大致相同。这种近似容易出错。这种凹凸不会彼此遮蔽,也不会有投影。我们想在这个像素点看到的是,观察向量和高度场的第一个交点。

         为了更好地解决这个问题,一些研究人员建议使用光线沿着观察向量行进(ray marching),直到找到一个(近似的)交点。 这可以在像素着色中完成,其中高度数据可以通过访问纹理得到。以这样或那样的方式利用光线的技术被归纳为视差映射技术的一个子集。

         这些类型的算法被称为视差遮盖贴图(parallax occlusion mapping,POM)浮雕贴图(relief mapping)方法,或其他名称。 关键的思想是先沿着投影向量测试一个固定数量的高度场纹理样本。对于掠射角度的观察射线,通常会生成更多的样本,这样就不会错过最近的交点。 沿着射线的每个三维位置都会被检索到, 转换到纹理空间,并处理,以确定它是在高度场的上面或下面。 一旦找到了高度场下面的一个样本,它往下的距离,以及之前的样本在上面的距离,会被用来找到交集位置,如图6.39所示。 然后,使用附加的法线贴图、颜色纹理和任何其他纹理,使用该位置对表面进行着色。高度追踪算法(heightfield tracing approach)同样会对凹凸不平的表面有投影。如图6.40所示。

图 6.39 从眼睛发出的绿色光线投影到表面所在屏幕, 每隔一定的时间采样一次(紫色的点),然后获取高度。 该算法求出了人眼发出的射线与拟合曲线高度场的黑色线段的第一个交点。 

图 6.40 未使用光线行进的视差贴图(左图)和使用了光线行进的视差贴图的比较(右图)。左图立方体上面是平坦的,而使用了光线行进的右图的立方体上面是有遮蔽效应的。

图 6.41 法线纹理和浮雕纹理。法线纹理没有自我遮蔽发生。 浮雕贴图在重复纹理的轮廓上有问题,因为矩形更多的是进入高度场的观察,而不是真正的边界定义。

图 6.42  视差遮蔽贴图,又名浮雕贴图,让道路上的石块更具有真实感。 地面实际上是一组应用了高度场的简单的三角形。

6.9 纹理光照(Textured Lights)

         纹理也可以用来增加光源的视觉丰富性,并允许复杂的强度分布或聚光灯功能。 对于把所有的照明都限制在圆锥或截锥体上的光,投射纹理可以用来调节光的强度, 这允许聚光灯有形状,有图案的光,甚至“幻灯片放映机”的效果(图6.43所示)。 这些光通常被称为gobo或cookie光。

图 6.43  投影纹理光(Projective textured light)。 纹理被投射到茶壶和地平面上,用来调节光在投影平截头体中的贡献(在平截头体外被设置为0)。

        对于不局限于截锥体,而是向各个方向照射的光线,可以使用一个立方体贴图来调节强度,而不是二维投影纹理。 一维纹理可以用来定义任意距离的衰减函数。 结合一个二维的角度衰纹理,这可以考虑用于复杂的体积照明模式。 一个更普遍的可能性是使用三维(体积)纹理来控制光线的衰减。 这允许任意体积的效果,包括光束。这种技术是内存密集型的(就像所有的体纹理一样)。如果光的体积效果沿着三个轴都是对称的, 通过将数据镜像到每个八分圆,这样将内存占用减少8倍。

         纹理可以添加到任何光类型,以启用额外的视觉效果。纹理光允许艺术家们简单控制光照,他们可以简单编辑使用的纹理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章