《Real-Time Rendering》纹理

一个表面的纹理就是它的外表,可以想象成一幅油画画在画布上。在计算机图形学中,纹理映射是一个使用一些图片、函数或其它的数据源在表面的每个位置上修改外表的过程。例如,要表示一个砖墙的话,首先可以用精确的模型来表示,不过这样的模型会包含大量的顶点,使用纹理的话,只需要将一张砖墙图片附着在一个四边形上,只需要2个三角形。观察这个四边形的时候,颜色图片就会出现在四边形的某一位置上。除非靠近仔细观察,否则很难看出几何细节的缺失。

然而,有些使用纹理的砖墙表面,除了出现的精度缺失问题,还会有其它令人难以信服的原因。例如,灰泥层本应是粗糙的,然而整个砖墙看起来却是有光泽的,观察者就会发现两个本应该不同的材质粗糙度却相同。为了创造出更真实的效果,就会使用第二张图片作为纹理赋予表面。这张纹理图片不改变颜色,而是改变墙面的粗糙度,并取决于表面的位置。现在砖块和灰泥层有着相同的颜色纹理,它们的粗糙度来自另一张纹理。

观察者现在可以发现,所有的砖块部分有光泽,灰泥层没有,但会发现整个砖块都是平坦的,这看起来并不正确,毕竟砖块表面是不平坦的。通过使用凹凸贴图技术,渲染时,砖块表面的法线就会各自不同,这样的话,表面看起来就不会完全光滑了。这种纹理会基于原始表面法线方向各自改变,用于计算光照。

从一个小的角度进行观察的话,凹凸光照感会被破坏,会看不太出来凹凸感。甚至用一个垂直角度观察,砖墙部分应该向灰泥部分投射阴影。视察贴图使用一张纹理来在渲染时变形表面,视察遮蔽贴图根据一张高度纹理放出射线来提升真实感。置换贴图技术是真正地根据纹理来修改三角形高度来建模。下图显示了一个使用颜色纹理和凹凸贴图的例子:



使用更多更高级的算法,可以用来解决许多纹理的不同类型的问题。在这一章,我们会很细节地说明纹理技术。首先,展示纹理映射过程,接着,着重图片纹理,然后会稍微介绍一下程序纹理,接着会介绍一些表面纹理效果。

纹理管线

纹理是一种高效构建表面材质的技术,一种考虑纹理的方式是考虑单个着色像素的情况。在之前的章节介绍过,着色是将材质和灯光的颜色考虑在内进行计算的,包括一些参数。如果存在的话,透明度也会影响样本。纹理通过修改在光照等式中的值来完成工作。这些值改变的方式通常基于表面上的位置。因此,对于我们的砖墙例子,在表面上每一点的颜色被砖墙图片上的相应颜色替代。图片纹理中的像素常称为纹素,用来区分屏幕上的像素。粗糙度纹理修改粗糙度值,凹凸纹理修改光照法线的方向,因此这些值可以改变光照等式的结果。

纹理可以通过一个一般性纹理管线描述。大多数的术语会在这里介绍,记住,管线中的每一部分都会详细介绍。

空间中的位置是管线的开始点。这个位置可以在世界空间中,但更通常的是在引用模型帧上,这样模型移动时,纹理会跟着移动。使用Kershaw的术语的话,这个空间中的点然后回被赋予一个投影器函数,来获取一个数字集,被称为纹理坐标,然后会用于访问纹理。这个过程被称为映射,这一词语衍生出纹理映射的术语。有时纹理图片被称为纹理贴图。

在这些值可以用来访问纹理前,一个或多个通信函数回用来转换纹理坐标到纹理空间中。这些纹理空间位置被用来从纹理中获取值,例如,他们可以是图片纹理的队列索引,用来获得像素。获得的值然后会通过一个值转换函数转换,最后这些值被用来修改表面的一些属性,例如材质或光照法线。下图细致地展示了这一过程。



这种管线复杂性的原因是,每一步骤都可以提供一个有用的控制,需要注意并不是所有的步骤需要一直开启。


使用这个管线的过程,见上图,一个三角形使用一个砖墙纹理,一个采样在其表面生成。在物体局部索引帧中的位置被建立,假设是(-2.3,7.1,88.2),一个投影器函数接着作用在这个位置上。和投影操作类似,将真实世界的三维物体映射到二维,投影器函数通常将(x,y,z)矢量转换为两元素的(u,v)矢量。这个例子使用的投影器函数和正交投影类似,就像幻灯片一样将砖墙图片作用到三角形表面上。为了恢复为砖墙,表面上的一个点可以被转换为一对范围为0到1之间的值,假设获取值为(0.32,0.29),这些纹理坐标被用来寻找该位置上的图片颜色。砖墙纹理的分辨率,假设为256x256,通信函树将(u,v)分别与256相乘,得到(81.92,74.24)。除去小数部分,像素(81,74)在砖墙图片上的颜色就是(0.9,0.8,0.7)。纹理颜色在sRGB空间,所以如果颜色要想在光照等式中使用,需要转换到线性空间,得到(0.787,0.604,0.448)

投影器函数

纹理过程的第一步是获取表面位置,将其映射到纹理坐标空间,通常是二维空间。建模工具包通常允许艺术家每个顶点定义(u,v)坐标,这些可以通过投影器函数或者网格拆解算法初始化。艺术家可以像编辑顶点一样编辑(u,v)坐标。投影器函数通常将空间中的三维点转换为纹理坐标,通常在建模程序中使用的函数包括球面、圆柱和平面投影。

其它的输入也可用于投影器函数。例如,表面法线可以用来选择6个平面投影方向中的方向的哪一个会用于表面法线。在面与面的相接处会产生纹理匹配的问题。Geiss讨论了一种混合边界的技术。Tarini等人描述了一种多边形立方体贴图技术,其中一个模型会使用一系列立方体投影进行映射,使用不同体积大小的立方体。

其它的投影函数本质上并不是投影,而是复杂的表面创建以及细分。例如,等参线曲面有一个UV集作为它的定义的一部分。见下图:


纹理坐标也可以通过任何不同的参数类型生成,例如视角方向、表面温度或其它可以想象的参数。投影器函数的目标是生成纹理坐标。将这些信息作为位置的函数仅仅是一种解决方式。

非交互式渲染器经常自己将这些投影函数作为渲染过程的一部分。对于整个模型,单个投影器函数应该足够了,但通常艺术家会使用工具来拆分模型和单独应用不同的投影器函数。见下图:


在实时渲染中,投影器函数通常会在建模阶段使用,投影的结果会存储在顶点中。这通常比较正确,有时在顶点或像素着色器中应用投影器函数比较有优势。这么做可以增加精度,并且可以帮助启用不同的效果,包括动画。一些渲染方法,例如环境贴图,有一些逐像素执行的特定投影器函数。

球面投影将点投射到一个以一些顶点作为中心的假想的球面上。这种投影和Blinn和Newell的环境贴图方式使用的类似。这种投影方法的问题在于顶点插值。

柱形投影计算u纹理坐标的方式和球面投影一样,v纹理坐标表示为沿圆柱轴的距离。这种投影对那些自带一个轴向的物体来说非常有用。当表面近乎垂直于柱体轴时,会发生失真的现象。

平面投影类似于X射线,沿着某一方向平行投影,将纹理应用到所有表面上,它使用正交投影,这种类型的投影在进行贴纸操作时会很方便。

当表面相对于投影方向是侧向时,会产生很严重的变形,艺术家经常必须手动将模型分解为近平面的部分。通常也有一些工具通过拆分网格来帮助减少变形,或者创建一个近似理想的平面投影集,或者其它的方式。目标是对每个多边形给定一个更公平的共享纹理区域,同时尽可能保持网格间的链接形。连接性是非常重要的,因为在网格边的接缝处一张纹理的不同部分会相遇。一个良好拆分的网格也可以让艺术家的工作更为简单。下图展示了创建某一雕像的工作环境:


这一拆分过程是一个更大研究领域的一个方面,即网格参数化。感兴趣的读者可以查看Hormann等人的SIGGRAPH的课程笔记。

纹理坐标空间不总是一个二维平面,有时它是一个三维体,在这一情况下,纹理坐标被表示为三元素矢量,u,v,w,w是沿投影方向的深度。其它的系统使用至多4个坐标,通常表示为(s,t,r,q),q作为齐次坐标。它就像是一个电影或幻灯片投影器,投影纹理的坐标随距离变化。举个例子,投影到一个装饰性聚光灯组件会很方便,称为gobo(遮光黑布),(这个应该就是电影幕布或者幻灯片投影幕布)

另一种重要的纹理坐标空间是方向性的,空间中的每一个点可以访问得到一个方向。一种可视化这种空间的方法是假设点在单位球体上,每一个点的法线就代表获取纹理位置的方向矢量。使用方向性参数化的一种最重要的纹理类型就是立方体纹理。

值得注意的是一维纹理图片和函数有它们的使用场景。例如,在地形模型上,颜色可以通过海拔高度决定,例如,低海拔地区可以是绿色的,山峰可以是白色的。线段也可以使用纹理,一种是使用方式是将雨滴作为长线段,使用一张半透明纹理渲染。这样的纹理在将某一值转化为另一种值是也很有用,例如,查找表。

因为多张纹理可以用于表面上,就需要定义一个纹理坐标集,这些纹理坐标会沿表面插值,可以被用来获得纹理值。在插值前,然而,这些纹理坐标需要通过通信函数进行转换。

通信函数

通信函数将纹理坐标转换到纹理空间位置。它们为应用纹理到表面的过程提供了便利性。一种通信函数的例子是使用API来选择一个已存在的纹理的一部分来显示。只有这种子图片会在接下来的子序列操作中使用。

另一种通信函数的类型是矩阵变换,可以在顶点或像素着色器中应用。这一函数可以在平移,旋转,缩放,剪切或投影纹理到表面上等操作中开启。在变换那一章中介绍过,变换的顺序也是会造成影响的。惊喜地是,对于纹理的变换的顺序必须是预期变换的逆序,这是因为纹理变换实际上影响的是决定纹理是否可以看见的空间。图片本身并不是一个要被变换的物体,而是定义图片位置的空间被改变。

另一类通信函数控制应用图片的方式。我们知道图片会在表面的[0,1]范围内的[u,v]坐标上显示,但在这一范围外呢?通信函数决定这一行为。在OpenGL中,这种类型的通信函数被称为包裹模式,在DirectX中,被称为纹理定位模式。通用的这种类型的通信函数是:

  • 包裹(DirectX)、重复(OpenGL)或拼贴——图片会沿表面重复自己,算法上来说,纹理坐标的整型部分被丢弃。这种函数对于生成重复纹理材质来说非常有用,大部分情况下也是默认的选择。
  • 镜像——图片沿着表面重复自身,但每次重复的时候会镜像翻转。例如,图片通常会从0到1出现,然后1到2之间翻转,以此类推。这对于纹理的边界提供了一定的连续性。
  • 夹紧(DirectX)或夹紧到边(OpenGL)——在[0,1]范围外的值会压缩到范围内,结果就是会重复纹理边的值。这一函数在进行纹理边的二次插值采样时可以避免一些问题。
  • 边界(DirectX)或夹紧到边界(OpenGL)——在[0,1]范围外的纹理坐标会使用一个单独定义的颜色进行渲染,这一函数可以将贴纸渲染到单色表面上,例如,纹理的边会和边界颜色进行平滑的混合。

上述四种函数的效果见下图:


这些通信函数可以在每个纹理轴上进行设置,例如,纹理可以沿u轴重复,可以在v轴上夹紧。在DirectX中,还有一个镜像一次的模式,会将纹理沿0坐标值进行一次镜像,然后夹紧,这对于镜像贴纸来说非常有用。

纹理的重复拼贴是一种为场景添加细节的简洁的方法,然而,这种技术通常会在重复三次以上时变得不那么可信,因为人眼可以看出来。一种通用的避免这样的周期性问题的解决方法是将这样得到纹理值和其它的非拼贴纹理进行结合。这种方法可以进行相当规模的扩展,可以在Andersson描述的商业地形渲染系统中看到。在这样的系统中,多重纹理基于地形类型、海拔、斜坡和其它因数进行结合。纹理图片也可以与几何图形进行绑定,例如灌木和岩石,它们放在场景的上面。

另一种避免周期性问题的选项是使用着色器程序来实现特殊的通信函数,来随机重新结合纹理模块或拼贴。Wang拼贴是这种方式的一个例子,Wang拼贴集是一些匹配了边的四边形拼贴集,拼贴追在纹理映射过程中随机选择。Lefebvre和Neyret实现了类似的通信函数,使用了相关纹理的阅读和图表来避免模块重复。

最后一种通信函数的应用比较复杂,它从图片的大小中获取值。纹理通常在[0,1]u,v范围内进行纹理应用。就像之前展示过的砖墙纹理例子,通过将该范围内的纹理坐标与图片的分辨率相乘,可能得到像素的位置。将(u,v)值限制在[0,1]范围内的优点是不同分辨率的纹理图片可以不需要改变存储在模型顶点中的值就可以随意交换。

纹理值

在使用通信函数来生成纹理空间坐标后,纹理坐标可以用来获得纹理值。对于图片纹理,这通过访问纹理来从图片中获取纹素信息来完成。这一过程在下一节中详细介绍。图片纹理映射在实时渲染中占据很大的比重,但程序化函数也可以使用。在程序纹理映射的例子中,从纹理空间位置中获取纹理值的过程并不包含内存查找的过程,而是使用一个函数进行计算。程序纹理会在之后的小节进一步讲解。

最直接的纹理值是一个RGB元组,可以用来替换或修改表面颜色。类似地,单色灰度值也可以得到。另一种数据类型是RGBA,这在上一章介绍过,\alpha值通常作为颜色的不透明度,决定了其覆盖的颜色是否会影响该像素。也就是说,任何其它的值都可以被存储在一张纹理中,在接下来的小节中会介绍凹凸映射技术,它存储的就是法线信息。

从纹理中获得值可以有选择地在使用前进行变换。这些变换可以在着色器程序中进行。一个通常的例子是将值的范围从[0.0,1.0]重映射到[-1.0,1.0],这个可用于将法线存储在纹理中。

图片纹理映射

在图片纹理映射中,一张二维图片会粘在一个或多个三角形上。我们已经讲解过了计算纹理空间位置的过程,现在我们可以使用这些流程和算法来从图片纹理中根据给定的位置获得纹理值。在本章的剩余部分中,图片纹理会简称为纹理。额外地,当我们提及一个像素单元时,我们的意思就是该像素的邻近屏幕网格单元。在上一节我们介绍过,像素实际上是一个可以影响其绑定像素网格单元采样的可现实颜色值的一个基本单位。

一个像素着色器传入纹理坐标值,并将其称为texture2D,来获取纹理值。这些在(u,v)纹理坐标中的值,通过一个通信函数映射到[0,1]上。GPU负责这一转换。在不同的API中有两种不同的主要纹理坐标系统。在DirectX中,纹理的左上角为原点(0,0),右下角为(1,1)。这与多种图片格式存储数据的方式相同。在OpenGL中,纹素(0,0)位于左下角,y轴与DirectX镜像翻转。纹素有整型坐标,但我们通常希望得到纹素间的位置并进行混合。这就带来的新的问题,浮点坐标代表哪一个像素。Heckbert讨论了两种可能的系统:截断和凑整。DirectX 9中将中心点定义在(0.0,0.0),使用凑整方式。这种系统会让人有些困惑,左上角像素的左上角时DirectX的中心,值是(-0.5,-0.5)。DirectX 10 向前改变为OpenGL的系统,纹素的中心有一个小数值(0.5,0.5)——截断,或更准确地说,向下取整,小数部分被丢弃。向下取整是一个一个更自然的系统,可以很好的与语言映射在一起,例如,像素(5,9),对于u坐标定义的范围是(5.0, 6.0),v坐标的范围是(9.0, 10.0)。

一种解释这一点的术语是从属纹理读取,有两种定义。第一种方式特别用于移动设备。当通过texture2D或类似的方式访问纹理时,从属纹理读取会发生,像素着色器会计算纹理坐标而不是使用未修改的从顶点着色器中传入的纹理坐标。注意这意味着会完全改变输入的纹理坐标,甚至是简单地例如交换u和v值的操作。老一点的移动设备GPU,比如那些不支持OpenGL ES3.0的设备,当着色器没有从属纹理读取时会更有效率,因为纹素数据可以预载入。其他的,更老一些的,这种数据的定义对于早期的桌面级的GPU尤其重要。在这样的环境中,从属纹理读取会在纹理坐标取决于一些之前的纹理值的结果时发生。例如,一张纹理可能会改变光照法线,也可以改变用来访问立方体贴图的坐标。这样的功能在早期的GPU中有限制或甚至完全不存在。如今这样的读取会对性能产生影响,取决于在片中基于其它因数计算的像素数。

在GPU中使用的纹理图片的大小通常是2^m\times 2^n个纹素,其中m和n为非负整数,也可以称为二次幂纹理。现代GPU可以处理任意大小的非二次幂纹理,可以将生成的图片当作纹理看待。然而,一些较老的GPU可能不支持非二次幂纹理的多级渐进纹理映射。图形加速器有着不同的纹理大小上限。例如DirectX12支持16384^2个纹素。

假设我们有一张256\times 256纹素大小的纹理,我们想要将这张纹理在一个四边形上使用。只要投影到屏幕上的四边形的大小大致等于纹理的大小,四边形上的纹理就看起来和原始图片几乎一致。但如果投影四边形覆盖原始图片10倍大小的像素(放大操作)或者覆盖原始图片很小的一部分(缩小操作)会发生什么?答案是取决于使用的采样类型和滤波方法。

在之前章节介绍过的采样和滤波方法会应用于从纹理中读取到的值上。然而,预期的结果是避免最终结果图片出现走样现象,这在理论上要求在最终像素颜色上进行采样和滤波。这里的区别在于两者,一是对光照等式的输入进行滤波操作,而是对其输出进行滤波操作。只要输入和输出线性相关(这对颜色这类值来说是正确的),那么单独对纹理值进行滤波操作和直接对最终颜色进行滤波操作,两者之间等价。然而,许多着色输入值存储在纹理中,例如表面法线和粗糙度,它们和输出值非线性相关。标准纹理滤波方法可能不会对这些纹理起效,结果可能是走样的。

放大

见下图:


一张大小为48\times 48个纹素的纹理投射到一个四边形上,相对于纹理大小,四边形看起来相当接近,所以底层的图形系统必须放大纹理。最通用的放大滤波技术是最邻近滤波和二次插值,也存在三次卷积,它使用4\times45\times5纹素阵列的权重和。这可以提升放大的质量。虽然原生硬件对三次卷积的支持并不那么完善,但可以在着色器程序中实现。

在上图的左边,使用的是最邻近方法。这种放大技术的一个特性是单个纹素会变得很明显。这种效果可以成为像素化,之所以发生是因为该方法在放大时会获取距中心像素最近的纹素颜色,结果就是这种块状图像。使用这种方法后的图像的质量不大好,因为每个像素只会直接获取一个纹素的值。

上图的中间,是使用二次插值的结果。对每个像素,使用这种滤波方式的话,会寻找周围邻近的4个纹素,然后二维线性插值找到一个混合颜色值赋予像素。结果是模糊的,同时粗糙感消失。做个小试验,对于上图左侧的那张图,试着斜视,会发现结果和使用低通滤波的结果类似。

我们返回之前介绍过的砖墙纹理例子,不丢弃小数部分,我们得到某一点(p_u,p_v)=(81.92,74.24)。我们这里使用OpenGL以左下角为原点的规则,因为这和笛卡尔坐标系一致,比较好理解。我们的目标是在4个最近纹素间插值,使用它们的纹素中心定义一个纹素大小的坐标系统。见下图:

为了找到4个最近像素,我们用采样位置减去像素中心小数部分(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')的二次插值颜色可以通过2个步骤进行计算。首先,下方的纹素,t(x,y)t(x+1,y),使用u'沿水平方向插值,最上方两个纹素类似,t(x,y+1)t(x+1,y+1)。对于底部的纹素,我们得到(1-u')t(x,y)+u't(x+1,y),对于顶部,(1-u')t(x,y+1)+u't(x+1,y+1)。这两个值然后使用v'进行竖直方向的插值,所以在位置(p_u,p_v)的二次线性插值的颜色b为:

b(p_u,p_v)=(1-v')((1-u')t(x,y)+u't(x+1,y))\\ +v'((1-u't(x,y+1)+u't(x+1,y+1))\\ =(1-u')(1-v')t(x,y)+u'(1-v')t(x+1,y)\\ +(1-u')v't(x,y+1)+u'v't(x+1,y+1)

直观地,一个像素越靠近我们的采样位置,它对最终值的影响越大。这就和我们在等式中看到的一样。右上(x+1,y+1)的纹素权重为u'v'。注意对称性:右上的权重和左下角与采样点一致。返回我们的例子,这意味着从纹素中获得值会和0.47\times0.74相乘,即0.3108。以这个纹素的顺时针看,其它的乘为0.42\times0.26,0.58\times0.26,0.58\times0.74,所有的权重加起来为1.0。

一种帮助进行放大操作的模糊的方法是使用细节纹理,这种纹理代表表面良好的细节,手机上的划痕或者地形上的灌木丛。这样的细节叠在放大的纹理上,作为一个单独的纹理,缩放级别也不同。将高频重复的细节纹理与低频的放大纹理结合,效果和高精度纹理类似。

二次线性插值在两个方向上插值。然而,线性插值并不是必须的。假设一张纹理包含黑白棋盘格单元,重映射后,假设低于0.4的灰色为黑色,高于0.6的灰色为白色,中间的值拉伸来填补沟壑,那么纹理看起来又像是棋盘格了,纹素间也进行了一些混合,见下图:


使用高精度纹理可以得到类似的效果。例如,想象一下每个棋盘格四边形包含4\times4的纹素,在每个棋盘格单元的中心,插值的颜色会非黑即白。

见上面的人像图的最右侧,使用了双三次滤波,残留的砖块感被大部分移除。需要注意的是,双线性滤波器的开销大于双线性滤波器。然而,许多高阶滤波器可以表示为重复的线性插值。总结一下就是,支持纹理单元的线性插值的GPU硬件可以使用一些查找来利用。

如果觉得双三次滤波开销过大,Quilez提出了一个简单的技术,使用平滑曲线来对2\times2纹素集进行插值。我们首先描述这样的曲线,然后介绍技术。两种常使用的曲线是平滑插值曲线和五次方曲线:

s(x)=x^2(3-2x)\quad smoothstep

q(x)=x^3(6x^2-15x+10)\quad quintic

当我们想要在值和值之间平滑插值时,这两个方法很有用处。平滑插值曲线有一个属性,s'(0)=s'(1)=0,在0到1之间平滑。五次方曲线有相同的属性,不过同时q''(0)=q''(1)=0,即开头和结尾的二阶导数均为0。两个曲线显示在下方:

这一技术通过首先将采样乘以纹理维数然后加0.5来计算(u',v')。整型部分被保留用作接下来的计算,小数部分存储在u'v',范围在[0,1](u',v')接着这么变换:(t_u,t_v)=(q(u'),q(v')),范围也在[0,1]内。最后,减去0.5,整型部分加回去。结果的u坐标然后除以纹理宽度,v同理。这样,新的纹理坐标与GPU提供的双线性插值查找一起使用。注意这一方法会在每一纹素上得到一个平台,意思是如果纹素在RGB空间上的某一平面上,然后这样类型的插值会得到平滑但阶梯式的效果,这也许不是预期的结果,见下图:

缩小

当纹理缩小时,一些纹素可能会覆盖一个像素单元,见下图:


为了让每个像素得到正确的颜色值,我们需要对影响像素的纹素的权重进行积分操作。然而,精确决定围绕某一确定像素的纹素的权重是很困难的,在实时渲染中更是完全不可能做到完美。

由于这种限制,GPU上会使用一些不同的方法,一种方法是使用最邻近,和对应的放大操作滤波类似,即它选择最靠近像素单元中心的纹素。这一滤波器可能会导致严重的走样问题。见下图:


最邻近在最上面的图使用,沿水平方向,会出现锯齿感,这是因为只从许多影响像素的纹素中选择了一个纹素来代表平面。这样的锯齿感在表面移动时会更明显,这一现象可以成为暂存走样。

另一种经常使用的滤波器是双线性插值,和放大操作中的类似。这一滤波器只比最邻近方法的效果好那么一点。它混合像素周围的四个纹素,但是当超过4个纹素会影响中心像素时,滤波器会失效,产生走样。

同样,存在更好的方法。在上一章介绍过,走样的问题可以通过采样和滤波技术解决。一张纹理的信号频率取决于在屏幕上显示的纹素的间隔大小。由于尼奎斯特限制,我们需要确保纹理的信号频率不大于采样频率的一半。例如,假设一张图片由交替的白线和黑线组成,波长就是两个纹素的宽,所以频率是1/2。为了恰当地在屏幕上显示这张纹理,频率至少要为2\times \frac {1} {2},即至少每个纹素覆盖一个像素,所以说,为了反走样,每个像素上至少要有一个纹素。

为了达成这一目的,像素采样频率要增加或纹理频率要减少。在前一章介绍过的反走样方法给出了一些方式来提升像素采样速率。然而,这只能有限制的增加采样频率。为了完全解决这一问题,目前已经得到了许多不同的纹理缩小算法。

纹理反走样算法的基础理念是:预处理纹理,并且创建可以帮助快速计算一个像素上的纹素集的近似效果的数据结构。对于实时渲染,这些算法的特性是是由固定的时间和资源来执行任务。使用这种方法的话,每个像素会得到一个固定数量得采样,并结合在一起计算一定数量的纹素的效果。

Mipmapping

最流行的纹理反走样方法是mpimappingmip的意思是小中见大(multum in parvo),拉丁语中的意思是在很小的地方中有很多东西。这个名字很适合命名这一过程,即原始的纹理被滤波操作后,重复为更小的图片。

当mipmapping缩小滤波被使用时,原始的纹理会在实际的渲染发生前使用一些更小版本的纹理来扩大。纹理会降低采样到一个原始区域的四分之一大小,每个新的纹素值通常被计算为原始纹理中的四邻像素的平均。新的第一级纹理有时被称为原始纹理的子纹理。缩减操作会递归执行直到纹理的一维或二维等于一个纹素。这一过程在下图展示:


这些生成的图片集被称为一个mipmap链。

在组成高质量mipmaps时最重要的两个元素是良好的滤波以及伽马校正。最通用的形成一个mipmap级别的方式是将每个2\times 2纹素集的值平均起来得到mip纹素值。使用的滤波器是一个盒装滤波器,这是最差的滤波器。它的结果质量很差,因会导致走样问题,最好使用高斯、兰索斯、凯撒或类似的滤波器。另外,一些图形API本身就在GPU上就提供了一些不错的滤波器。在纹理边处,必须注意在滤波期间是要纹理重复还是单个复制。

对于非线性空间中的纹理编码,在滤波时忽略伽马校正会修改mipmap的可见亮度。当我们远离物体时,使用不正确的Mipmap级别的话,物体会整体看起来很暗,对比度和细节也会受到影响。针对这一理由,将这样的纹理从sRGB转换到线性空间是很重要的,在线性空间进行所有的mipmap滤波,将最终的结果转换为sRGB空间。大多数API支持SRGB纹理,所以可以在线性空间生成正确的Mipmap,并且将结果存储在sRGB中。当SRGB纹理被访问时,它们的值首先转换到下线性空间,这样放大和缩小操作会恰当执行。

在之前提到过,一些纹理与最终的着色颜色有着非线性的关系。尽管这在一般情况下会导致滤波的问题,mipmap的生成对这一问题非常敏感,因为成百上千的像素会被滤波。特定的mipmap生成方法可以得到最好的结果。

在纹理映射时访问这一结构的基本过程是很直接的。一个屏幕像素围住纹理上的一片区域。当像素区域映射到纹理后,它包含一个或多个像素。使用像素网格边界并不能确保完全正确,但在这里使用可以很简单地进行展示。网格外的纹素可以影响像素的颜色。这一过程的目的是粗略的决定纹理影响像素的程度。有两种通用的方法,它们计算d的值(OpenGL称为\lambda,也可称为纹理的细分级别)。其中一种方法像素网格的四边形的最长的边来近似模拟像素的覆盖。另一种方法使用4个微分值中的最大值来进行测定,du/dx,dv/dx,du/dy,dv/dy。每个微分描述了每个轴向上纹理坐标的改变量。例如,du/dx是每个像素沿x屏幕轴向u纹理值得变化量。更多的细节可以看一下Flavell和Pharr或Williams的文章,McCormack等人讨论了使用最大值方法的反走样,他们展示了一种交换公式。Ewins等人分析了几种比较算法的硬件消耗。

使用Shader Model3.0或更新的版本,像素着色器可以获得这些梯度值。由于这些值基于邻近像素值的差来计算,在动态流分支中,像素着色器无法访问这些值。对于在这样的环节中执行的纹理读取,导数必须提早计算。注意由于顶点着色器不能访问梯度信息,梯度或细分级别需要由顶点着色器自己计算,在进行顶点纹理映射的时候提供给GPU。

计算d坐标的目的是决定在哪里沿mipmap的三角锥轴采样。见下图:


目的是将像素比纹素的比例至少控制在1:1范围内,来实现尼奎斯特速率。这里比较关键的一点是当像素网格包含更多的纹素,d开始增长,就会访问到更小更模糊版本的纹理。(u,v,d)元组用来访问mipmap。d值用来模拟纹理级别,且是浮点数。(u,v)位置用来获取从两个纹理级别的每个级别的二次线性插值采样。采样结果然后线性插值,取决于每个纹理级别到d的距离。这整个过程被称为三次线性插值,逐像素执行。

对于d坐标的一个用户控制是细分级别偏移(LOD bias)。这个值加到d上,所以它会影响纹理可察觉到的清晰度。如果我们增加d值,纹理会看起来更模糊。一个对任意给定的纹理的好的LOD 偏移会随图片类型和使用方式变化。例如,那些从开始有点模糊的图片可以使用一个负的偏移值,而那种用于纹理映射的有锯齿感的图片可以使用正的偏移值。可以为整个纹理设置一个全局偏移,或者在像素着色器中逐像素设置。对于更好的控制,用于计算它的d坐标或导数可以由用户自己配置。

mipmapping的一个优势是,不用去尝试将所有的纹素合在一起去单个影响像素,而是访问和插值预结合的像素级。这一过程会花费固定的时间,无论有多少数量的缩小操作。然而,mipmapping有一些缺点,最主要的一个是过度模糊。想象一下一个像素网格,在u方向覆盖了大量的纹素,但在v方向只覆盖了少量的纹素。当观察者沿几乎与表面平行的方向观察时,就会发生过渡模糊的问题。实际上,有可能需要沿某一轴向进行放大,同时沿另一轴向进行缩小。访问mippmap的效果即获取纹理上的一个四边形区域,但不可能获得一个举型区域。为了避免走样,我们选择在纹理上的最大像素网格的近似覆盖测量值。在获得采样中结果通常会相对来说比较模糊。这一效果可以见下图:


区域求和表

另一个避免过度模糊的方法时使用区域求和表(SAT)。为了使用这一方法,我们需要首先创建一个数组,大小为纹理大小,但包含更高精度的颜色值。在数组的每个位置,我们必须计算和存储由该位置和(0,0)纹素处组成的矩形中的所有纹素的和。在纹理映射期间,像素网格投影到纹理的这一过程受矩形影响。区域求和表然后被访问来决定该举型的颜色,该颜色会回传作为像素的纹理颜色。平均值使用矩形的纹理坐标计算,见下图:


这是通过下面的公式计算的:

c = \frac {s[x_{ur},y_{ur}]-s[x_{ur},y_{ll}]-s[x_{ll},y_{ur}]+s[x_{ll},u_{ll}]} {(x_{ur}-x_{ll})(y_{ur}-y_{ll})}

其中,x和y是矩形的纹素坐标,s[x,y]是对该纹素的区域求和值。这一等式会将右上角的整体区域的和,减去区域A和区域B。区域C会减去两次,因此需要加上一次。

使用了区域求和表的结果显示在下图:


趋于水平的线在靠近右侧的边时会变得更尖锐,但中间的斜交叉线仍看起来是过渡模糊的。问题在于,当纹理沿斜侧观察时,会生成一个很大的矩形,那么会有大量位于像素附近的纹素参与计算。例如,想象一个长且细的矩形,表示一个像素网格的反投影区域,位于整个三角形的对角线上。那么整个纹理矩形的平均值会返回,而不是只返回像素网格的平均值。

区域求和表是各向异性滤波算法的一个例子。这样的算法会获取整个非矩形区域中的纹素值。然而,SAT可以在水平和垂直方向高效计算。注意,区域求和表还需要花费至少两倍的纹理内存,如16\times16大小或更小的,但对于更大的纹理就需要更高的精度了。

区域求和表,可以在现代GPU中实现。提升了性能的滤波方法对最新渲染技术的质量来说至关重要。例如,Hensley等人提供了一种高效的实现,展示了如何通过区域求和表来提升辉光反射的质量。其它的算法,可以通过SAT来提升区域采样的质量,例如景深,阴影贴图,模糊反射。

无约束各向异性滤波

对于当前的图形硬件,最通用的提升纹理滤波的方法是重复使用已存在的mipmap硬件。基本理念在于像素单元会反投影回去,这一纹理上的四边形区域然后会被采样多次,所有的采样会被结合。就如上面的概览所说,每个Mipmap有一个位置和一个近似方形的区域。该算法不适用单个mipmap来近似四边形的覆盖,而是使用多个矩形来覆盖四边形。四边形的短边可以用来决定d值。这会让平均区域对于每个mipmap采样来说变得更小。四边形的长边会用来创建平行于长边的各向异性线,穿过四边形的中间。当各向异性线的数量在1:1和2:1之间时,会获得沿线的两个采样。各向异性比率越高,会沿该轴获得更多的采样:


这一模式允许各向异性线沿任意方向伸展,所以并没有区域求和表的限制。它也不和mipmap一样要求更多的内存,因为它是使用mipmap的算法来进行采样的。各向异性滤波的例子如下:


这种沿一个轴采样的想法最先由Schilling等人随着他们的Texram动态内存设备提出。Barkans描述了在Talisman系统中的算法使用。一个类似的系统称为Feline,由McCormack等人提出。Texram的最开始的等式会让沿各向异性轴的采样获得相同的权重。Talsman在轴的两个末端的探针给定一半的权重。这些算法比较接近高质量的软件采样算法,如椭圆权重平均滤波(EWA),它将影响的像素区域转换到一个纹理上的椭圆区域,在椭圆中通过一个滤波核心来为这些纹素分配权重。Mavridis和Papaioannou展示了一些方法来使用着色器代码实现EWA滤波。

体积纹理

图片纹理的一个直接的扩展是三维纹理数据,可以通过(u,v,w)访问。例如,医疗图像数据可以生成为一个三维网格,通过该网格移动多边形,我们就可以观察到这些数据的二维切片。一个相关的想法是用这种方法表示一个体积光。在表面上的某一点的光照可以通过找到体积内的该位置的值结合光的方向来建立。

大多数的GPU对于体积纹理支持mipmapping。由于在一个体积纹理的单个mipmap级别内的滤波包含三次线性插值,在mipmap等级间的滤波要求四次线性插值。由于这包含平均来自16个纹素的值,因此可能导致精度问题,可以使用更高精度的体积纹理来解决。Sigg和Hadwiger讨论了与体积纹理相关的问题,提供了对于执行滤波和其它操作的有效率的方法。

尽管体积纹理由更高的存储要求,且滤波的话开销更大,但它们仍有一些独特的优势。找到一个三维网格的一个良好的二维参数化的复杂的过程可以跳过,因为三维位置可以直接作为纹理坐标使用。这避免了二维参数化过程中常会出现失真和接缝问题。一个体积纹理也可以用来表示例如木材或大理石材质的体积结构。一个使用这样纹理的模型就像是从该材质雕刻出来的。

对于表面纹理映射使用体积纹理是非常低效的,因为采样的最大的优势没有被使用。Benson和Davis和DeBry等人讨论了使用系数八叉树结构的存储纹理数据的方法。这一模式非常适配交互式三维纹理绘制系统,同时表面并不需要在创建时明确纹理坐标,八叉树可以在任何级别下都能保持细节。Lefebvre等人讨论了在现代GPU上实现八叉树纹理的细节。Lefebvre和Hoppe讨论了一种将稀疏体积数据打包为一个更小纹理的方法。

立方体贴图

另一种纹理的类型是立方体贴图,它有六个矩形纹理,每个纹理对应一个面。立方体贴图可以使用一个三维纹理坐标矢量来采样,它可以通过一个从立方体中心设想外部的射线的方向来标识。射线与立方体交叉的点被找到,然后做接下来的工作。纹理坐标的最大坐标可以用来选取相关联的面(如(-3.2,5.1,-8.4)对应-z面)。剩余的两个坐标会除以最大坐标值的绝对值。现在坐标的范围为-1到1,然后可以简单的映射到[0,1]用于计算纹理坐标。例如,坐标(-3.2,5.1)被映射到((-3.2/8.4+1)/2, (5.1/8.4+1)/2)≈(0.31,0.80)。立方体贴图对于使用方向的值来说非常有用,常用于环境贴图。

纹理表现

在一个应用程序中操作许多纹理时,有一些方法提升性能。纹理压缩会在下节讲解,这一节会着重在纹理图集,纹理数组和无绑定纹理,所有的这些的目的都在于避免在渲染时变换纹理的额外开销。

为了让GPU尽可能地批处理更多的工作,通常会尽可能减少改变状态的次数。为了这一目的,我们可以将一些图片合并到一个更大纹理中,称为纹理图集。这一结构在下图显示:


注意子纹理的形状可以是任意的。子纹理在纹理图集中的摆放位置的优化由Noll和Sricker描述。在生成和访问mipmap时也需要倍加小心,因为高级别的mipmap可能会包含一些独立的不相关的形状。Manson和Schaefer展示了一种方法来优化mipmap创建,通过考虑表面的的参数,可以生成更棒的结果。Burley和Lacewell展示一种称为Ptex的系统,其中细分表面中的每个四边形有它自己的小纹理。优势在于可以避免为整个网格赋予一个单独的纹理坐标,在接缝处也不会有粗糙感。为了对这些四边形使用滤波,Ptex使用了一个邻近数据结构。Hillesland展示了打包Ptex。它将每个面的子纹理放到一个纹理突击中,并使用邻近面来填补,避免滤波时的非间接取值。Yuksel展示了网格颜色纹理技术,它提升了Ptex的效果。Toth提供了对于类Ptex系统的沿所有面的高质量滤波,通过实现一个方法,即如果滤波阀在[0,1]^2范围外会被剔除。

使用图集的一个难点在于包裹/重复和镜像模式,它们不会恰当地影响子纹理,而是会影响整个纹理。另一个问题时当为一个图集生成mipmap时,一个子纹理可能会和另一个子纹理混合。然而,这可以通过某些方法避免,即在将子纹理放到一个大的纹理图集中前,对每个子纹理生成mipmap层级,对每个子纹理使用2次方分辨率。

对于这些问题,一个更简单的解决方法是使用纹理数组,它完全避免了对于mipmap和重复模式的问题。见下图:


在纹理数组中的所有子纹理需要有相同的维度、格式、Mipmap层级和MSAA设置。和纹理图集类似,对于一个纹理数组来说只需要初始化一次,然后可以在着色器中使用索引来访问任何数组中的元素。这比单独绑定每个子纹理快至少5倍。

一种可以帮助避免状态转换开销的特性是非绑定纹理。不使用非绑定纹理的话,一个纹理会绑定到一个特定的纹理单元。一个问题是纹理单元的数量会有限制,这会让程序员没有太大的自由。驱动确保了纹理会放置在GPU侧。使用非绑定纹理的话,就没有绑定纹理的上限,因为每个纹理与一个64位的指针绑定,有时成为一个句柄。这些句柄可以用许多不同的方式访问,如通过统一变量,变化变量,从其它纹理或从着色器的存储缓冲对象。应用程序需要确保纹理常驻在GPU上。非绑定纹理避免了驱动中的不同类型的绑定开销,可以让渲染更快速。

纹理压缩

一种可以解决内存、带宽和缓存问题的方法是固定速率的纹理压缩。通过使用GPU飞速解码压缩纹理,一个纹理就会要求更少的内存,可以增加缓存的大小。一个相关但不同的使用情景是使用压缩来能够使用更大的纹理。例如,一个非压缩纹理每纹素使用3字节,对于512^2大小的纹理,就会占用768kB。使用纹理压缩的话,如果压缩比例为6:1,一个1024^2大小的纹理只会占用512kB。

在图片文件格式中使用的图片压缩方法有很多,如JPEG和PNG,但在硬件中实现这些编码方式是很耗性能的。S3发展出一种模式,称为S3纹理压缩(S3TC),这一技术被DX采纳,称为DXTC,在DX10中还称为BC(块压缩)。此外,在OpenGL这也是标准的压缩技术,因为几乎所有的GPU都支持。它的优势在于创建的压缩图片在大小上是固定的,有相互独立的编码片,解码也很简单。每个图片的压缩部分可以单独地进行处理。也没有共享查找表或其它依赖,因此可以很简单地编码。

存在几种DXTC/BC压缩技术的变体,它们有一些相同的属性。编码在4\times4纹素块上完成,也成为拼贴。每个块单独编码。编码基于插值。对于每个编码的量,会存储两个索引值。对每个块中的纹素插值因数会被保存。它选取在两个引用值之间的一个值,如,在两个颜色间插值。这一压缩主要来自于每像素存储颜色的索引值。

实际上的编码有七种变体,在下面的表格显示:


注意DXT是DX9里的名字,BC是DX10后的名字。在表中可以读到,BC1有2个16位的引用RGB值(5位红、6位绿、5位蓝),每个纹素有一个两位的插值因数,用于选取其中一个引用值或中间值。对比为压缩的24位RGB纹理,BC1表示6:1的纹理压缩比率。BC2编码颜色的方式和BC1类似,但逐纹素添加了一个4位的量化的透明度值。对于BC3,每个块的RGB数据的编码和BC1一样,额外地,透明度数据使用两个8位索引值和一个逐纹素3位的插值因数编码。每个纹素可以选择索引透明度值中的一个或者6个插值中的一个。BC4是单通道的,类似于BC3中的透明度通道。BC5有两个通道,每一个通道和BC3的类似。

BC6H用于高动态范围的纹理,其中每个纹素的RGB通道一开始都是16位浮点数值。这一模式使用16字节,结果就是每个纹素8位。BC6H对于单线有一个模式,对于双线也有一个模式,其中每个块可以从一个小的分区集中选择。两个引用颜色可以被间隔编码来得到更好的精度,也可以有不同的精确度,取决于模式的使用类型。在BC7中,每个块可以有1到3条线,每个纹素存储8位。它的目标是8位RGB和RGBA的高质量纹理压缩。它和BC6H有一些共有的属性,但纹理格式是LDR的,而BC6H是HDR的。注意BC6H和BC7在OpenGL中分别被称为BPTC_FLOAT和BPTC。这些压缩技术也可以被赋予到立方体或体积纹理上。

这些压缩技术最主要的缺点是它们会失真,即我们很难再去通过压缩过的图片来获得原始的版本。在BC1-BC5中,只有4或8个插值被用来表示16个像素。如果拼贴有大量的可分辨的值,那么就会丢失一些信息。实际上,如果正确使用这些压缩技术的话,还是能够得到不错的高保真图片的。

BC1-BC5的一个问题在于,所有的用于块的颜色位于RGB空间中的一条直线上。例如,颜色红、绿、蓝不能之间在单个块中表示。BC6H和BC7提供更多的线,因此可以提供更高的质量。

对于OpenGLES,选择了另一种压缩算法,称为爱立信纹理压缩(ETC)。ETC和S3TC有着类似的特性,即快速编码、随机访问、非定向查找和固定速率。它将一个4\times4的的纹素块编码到64位,即每纹素使用4位。见下图:

每个2\times4块(或4\times2,取决于哪一个会得到最好的质量)存储一个颜色,每个块也从一个小的静态查找表中选择一个四常量集,块中的每个纹素可以选择加上表中的一个值。这会修改每个像素的亮度。图片的质量与DXTC同等。

在包含在OpenGLES3.0中的ETC2中,未使用的位结合用来添加更多模式到初始的ETC算法中。一个未使用的位结合是压缩的表现(如64位),可以解压为和另一个压缩表现的相同图片。例如,BC1中,将两个引用颜色设置为相同的这一操作是没有必要的,因为这会指示一个常量颜色块,接下来只要一个引用颜色包含这个颜色块,那就可以被获取。在ETC中,一个颜色也可以使用一个数字从第一个颜色开始进行间隔编码,因此,计算可能会上溢或下溢。这样的情况层用来标识其它的模式。ETC2添加了逐块拥有获取方式不同的4个颜色的两种新模式,最后的一个模式是RGB空间中的一个平面,用于处理平滑的转换。爱立信透明度压缩(EAC)压缩带有一个通道的图片。这种压缩类似于基本的ETC压缩,但只针对一个组件,结果的图片每纹素存储4位信息。它可以有选择地与ETC2结合,另外,两个EAC的通道可以用来压缩法线。所有的ETC1、ETC2和EAC是OpenGL4.0核心模式、OpenGLES3.0、Vulkan和Metal的一部分。

法线贴图的压缩需要多注意一些。为RGB设计的压缩格式通常并不能很好地适配法线数据。大多数的方法都遵循这么几点,即法线是单位长度的,并假设其z组件是正数(切线空间)。这样的话就只允许我们存储法线的x和y组件,z组件可以迅速算出来:

n_z = \sqrt {1-n_x^2-n_y^2}

这一存储方式对压缩来说有不错的效果,毕竟只存储了2个组件。由于大多数的GPU并不直接支持3组件的纹理,这也就避免了浪费一个组件的可能性。更进一步的压缩方式通常会将x和y组件存储在BC5/3Dc格式的纹理中,见下图:


由于每个块的索引值区分最大和最小的x、y组件值,它们可以定义为在xy平面上的一个bounding box。3位插值因数允许我们在每个轴上在8个值中进行选择,所以bounding box会被划分为法线可能出现的8\times8网格中。另外,EAC的两个通道可以被使用,存储x、y值,z值得计算方式和上面一致。

在不支持BC5/3Dc或EAC格式的硬件中,通常的备用方法是使用一个DXT5格式的纹理,存储两个组件到绿色和透明度通道中(因为这两个通道存储值最高),其余两个通道不使用。

PVRTC是Imagination Technologies公司的硬件PowerVR支持的一种纹理压缩格式,它通用于iPhone和iPad中。该格式逐纹素存储2和4位数据,压缩4\times4纹素的块。核心的理念是提供两个图片的低频信号,可以使用邻域纹素数据和插值来获得。然后整个图像中,对两个信号的插值中使用每纹素1或2位数据。

适应性可扩展纹理压缩(ASTC)是一种不同的技术,它将n\times m的纹素块压缩到128位。块的大小从4\times412\times12不等,结果是比特率不同,每纹素0.8位到每纹素8位。ASTC针对简洁的索引表示使用了许多技巧,线的数量和端点编码可以逐块选择。另外,ASTC可以处理任何1-4通道的纹理,包括LDR和HDR。ASTC是OpenGLES3.2及以下版本的一部分。

上述讲解的所有的纹理压缩方式都会失真,当压缩一张纹理时,在压缩过程中也会花费不同的时间。在压缩上花费数秒甚至数分钟,我们整体上可以得到比较高的质量。因此,这一过程往往是离线的预处理过程,会存储纹理供之后使用。相反,我们还可以只花费几毫秒在压缩上,结果的质量就很差,但纹理可以近乎实时地压缩,并且可以立即使用。因为是使用固定方法硬件来完成的,所以说解压操作是非常迅速的,这一区别被称为数据压缩不对称性,即压缩所耗费时间远高于解压。

Kaplanyan展示了一些可以提升压缩纹理质量的方法。对于包含颜色的纹理和法线贴图,建议贴图使用每组件16位存储。对于颜色纹理,我们可以执行一次直方图归一化,之后在着色器中会使用一个缩放和偏移常量来复原。直方图归一化是一种使用展开图片中使用的值来扩展整个范围,视觉上来看就是提升了对比度。每组件使用16位存储确保了在归一化后直方图中没有未使用的位置,可以避免条带感,这是大多数压缩技术会带来的问题。见下图:


另外,Kaplanyan建议纹理使用线性颜色空间,如果75%的像素的值超过166/255,否则就存储在SRGB空间中。对于法线贴图,他也标记到,BC5/3Dc经常会独立压缩x,不管y,这意味着并不能总是找到最佳的法线,相反,他建议使用下面的误差测量:

e=arccos( \frac {n \cdot n_c} {||n|| ||n_c||})

其中n是原始的法线,n_c是压缩法线,然后解压。

注意,也可以将纹理压缩到不同的颜色空间中,可以加速压缩过程。一个通用的转换是RGB->YCoCg:

\left(\begin{matrix}Y\\C_o\\C_g\end{matrix}\right)=\left(\begin{matrix}1/4&1/2&1/4\\1/2&0&-1/2\\-1/4&1/2&-1/4\end{matrix}\right)\left(\begin{matrix}R\\G\\B\end{matrix}\right)

其中Y是亮度术语,C_oC_g是色度术语。逆变换也很简单:

G=(Y+C_g),t=(Y-C_g),R=t+C_o,B=t-C_o

这两个变换都是线性的,注意上上面的矩阵,它本身就是线性的。所以说,我们可以将纹理存储在YC_oC_g中,纹理硬件仍在YC_oC_g中执行滤波,然后像素着色器中可以转化回RGB。需要注意的是这一转换过程也是会失真的。

还有另一个RGB->YCoCg的可逆变换:

\begin{cases}C_o=R-B\\t = B+(C_o >> 1)\\C_g = G-T\\Y=t+(Cg>>1) \end{cases} \Leftrightarrow \begin{cases} t = Y-(C_g>>1)\\G=C_g+t\\B=t-(C_o>>1)\\R=B+C_o\end{cases}

其中>>是右移符号。这意味着可以向前和向后变换。需要注意的是RGB中的每个通道有n位,Co和Cg有n+1位,来保证可逆变换可能性,Y只需要n位。Van Wavern和Castano使用了失真的YCoCg变换实现了在CPU和GPU上快速压缩到DXT5/BC3的方法。他们在透明度通道存储Y值(因为精度最高),Co和Cg存储在RGB最开始的两个通道。由于Y被单独存储和压缩,因此压缩速度非常快。对于Co和Cg组件,会找到一个bounding box,选择盒子的对角线来产生最佳的效果。注意对于在CPU上动态创建的纹理,最好也在CPU上压缩纹理。当纹理通过GPU渲染创建时,通常最好在GPU上压缩纹理。YCoCg变换和其它的亮度-色度变换通常在图片压缩中使用,其中色度组件通过2\times2像素邻域平均。Lee-Steere和Harmon进一步做出研究,通过将其转换到HSV空间,通过在x和y中使用4因数来降采样色相和饱和度,将值存储位单通道DXT1纹理。Van Waveren和Castano也描述了快速压缩法线贴图的方法。

一个来自Griffin和Olano的研究表明当一些纹理应用于拥有复杂光照模型的纹理上时,除了可见的区别外,纹理质量可能很低。所以,针对这一情况,纹理质量的降低是可以接受的。Fauconneau展示了一种DX11纹理压缩格式的SIMD实现。

程序纹理

给定纹理空间的位置,执行一次图片查找,这是一种生成纹理值的方式。另一个方式是估计一个函数,这可以定义一个程序纹理。

尽管程序纹理通常在离线渲染程序中使用,不过纹理本身完全可以在实时渲染中使用。由于现代GPU上极高效率的图片纹理硬件,使得可以在一秒内执行数百亿次纹理访问。然而,GPU的架构包含减少高开销计算和内存访问的目的,因此让程序纹理在实时渲染中能够更好的使用。

体积纹理是程序纹理的一种特别吸引人的应用,毕竟体积纹理会耗费较大的存储空间。这样的纹理可以由许多技术来合成。最通用的是使用一个或更多的的噪声函数来生成值。见下图:


一个噪声函数经常以连续的二次幂频率采样,称为八度。每个八度给定一个权重,通常会随频率增加而减少,这些权重采样的和称为一个湍流函数。

由于估计一个噪声函数的高开销,在三维数组中的晶格点通常会预先计算,然后用来插值纹理值。有一些使用颜色缓冲混合的方式来快速生成这些数组。Perlin展示了一种快速可行的采样噪声函数的方法,并展示了一些使用例子。Olano提供了允许在存储纹理和执行计算间权衡的噪声生成算法。McEwan等人开发出一些在着色器中不需要使用查找表的计算经典噪声的方法,源码也可获得。Parberry使用动态编码来使用几个像素来分摊计算,来加速噪声计算。Green给出了一个高质量的方法,但主要是针对于可交互应用程序的,因为对于一个查找使用了50个像素着色器命令。由Perlin提供的最开始的噪声函数可以进行扩展。Cook和DeRose展示了一种替换方法,称为微波噪声,避免了较小的求值计算开销增长带来的走样问题。Liu等人使用许多不同的噪声函数来模拟木材纹理和表面抛光。我们也推荐Lagae等人的艺术的状态的报告。

还存在其它的程序化方法。例如,多孔纹理,通过测量每个位置到空间中的散列特征点的距离来构成。将结果中的最近距离使用不同的方式进行映射,如改变颜色或着色法线,创建的组件看起来就像是细胞、石板、蜥蜴皮肤和其它自然纹理。Griffiths讨论了如何在GPU上高效寻找最近的邻域位置并生成多孔纹理。

程序纹理的另一种类型是物理模拟或一些交互过程的结果,例如水波或延伸裂缝。在这些情况下,程序纹理可以生成针对动态情况的不规则效果。

当生成程序化二维纹理时,参数设置可能会让其比单纯设计纹理要更复杂,毕竟对于伸展或接缝这类问题可以很方便的手动解决。一种方法是直接合成纹理到表面上来避免完全的参数化。在复杂表面上执行这种操作在技术上是很有挑战性的,关于这点可以查看Wei等人的研究。

反走样程序纹理在某些方面比反走样图片纹理困难,某些方面有比较简单。一方面,例如mipmapping的预计算方法并不可行。另一方面,程序纹理的制作者又可以获得纹理内容的内部信息,因此可以想方设法避免走样问题。这对于那些组合几个噪声函数的程序纹理非常适用。每个噪声函数的频率可以得知,所以任何可以导致走样问题的频率可以被剔除,这样也会减少计算开销。也有许多反走样其它程序纹理的技术。Dorn等人讨论了之前的工作,并展示了一些避免高频来重新组成纹理函数的过程,即有限带宽。

纹理动画

应用到表面的图片并不一定是静态的,例如,一个视频资源可以被当作纹理,随时间改变。

纹理坐标也并不需要是静态的,程序设计者可以精确地在帧与帧之间改变纹理坐标,可以在网格数据中或者通过在顶点或像素着色器中使用函数。想象以下一个瀑布模型被构建,并且已经赋予了看上去像是下落水流的纹理。假设v坐标是水流的方向,为了让水移动,我们必须在每个下一帧的v坐标中减去一个值。从纹理坐标减去值得效果是让纹理自己移动。

更多的效果可以通过将一个矩阵应用到纹理坐标上来完成。除了平移变换外,还允许线性变换,例如缩放、旋转和剪切,图像包裹和变形变换、和一般投影。更多复杂的效果可以通过在CPU中适用函数或在着色器中使用函数来完成。

通过使用纹理混合技术,我们可以实现其它的动画效果。例如,一个大理石纹理,在肉纹理上渐变,我们可以让一个雕像重现生机。

材质映射

纹理通用的用法是修改材质的属性,来影响光照等式。真实世界的物体通常由不同的材质属性,为了模拟这些物体,像素着色器可以从纹理中读取值并使用它们来修改材质参数,然后用于光照等式。通过纹理修改的值往往是表面颜色值,这样的纹理也被称为albedo颜色贴图或漫反射颜色贴图。然而,任何参数都可以使用纹理来修改:替换、相乘或者用某种其它的方式来改变。例如,在下图中,投三个不同的纹理被应用于表面,替换常量值:


在材质中的纹理的使用可以进一步讨论。处理在光照等式中修改参数,纹理还可以用于控制像素着色器本身的流和函数。两个或多个有着不同的光照等式和参数的材质可以通过一个遮罩纹理应用到同一个表面,通过其中的数据来控制表面的哪一部分使用哪一个材质。例如,一个拥有一些铁锈的金属表面可以使用一个纹理来指示哪些地方拥有铁锈,有选择基于纹理查找来执行着色器的铁锈部分,否则就是光滑的金属着色。

光照模型的输入,例如表面颜色,与最终着色器输出的颜色呈线性相关。因此,包含这些输入的纹理可以使用标准技术来滤波,避免走样。包含非线性输入的纹理,例如粗糙度或凹凸映射,在反走样的时候需要小心一点。考虑光照等式的滤波技术可以针对这些纹理提升结果的质量。

透明度映射

透明度值可以通过透明度混合或透明度测试来实现很多效果,如高效植物渲染、爆炸和远距离物体等。这一节讨论拥有透明度值纹理的使用,注意其使用的限制和破除限制的方法。

一种纹理相关的效果使贴纸。例如,加入我们想要将一张花的纹理贴在茶壶上,我们不想贴上整个图片,只想贴上那些花显示的地方。通过赋予一个纹素0的透明度值,我们可以让其变得透明,因此就没有效果。假设,通过一些恰当的设定,设置好了花贴纸纹理的透明度,我们可以使用这个贴纸在茶壶表面替换或混合纹理。通常,一个clamp通道函数会用来限制透明的边界,将一个贴纸应用到茶壶表面上。一个应用的例子如下:


一种类似的透明度的应用是设置剪贴。假设我们创建了一个灌木丛的贴纸图片,并将其应用到场景中的一个矩形上。理念和贴纸类似,只不过灌木丛纹理会绘制在任何几何体上。这样的话,使用单个矩形就可以渲染拥有复杂剪影的模型。

在灌木丛的例子中,如果我们旋转摄像机,就会发现灌木丛没有厚度。一种解决方法是复制一个相同的灌木丛四边形,然后旋转90度。两个矩形组成了一个低耗的三维灌木丛,优势称为交叉树,当在远处观察时效果还不错。见下图:


Pelzer讨论了一种类似的配置,使用三个裁切值来表示草。之后我们会讨论公告板技术,可以只渲染一个矩形来表示树。如果观察者靠得太近的话,就会发现破绽,从上往下看的话就会发现是两个面片。见下图:


为了解决这一点,会使用不同的方式添加更多的裁切——切片、分支、层——只为提供可信的模型。之后会介绍生成这样模型的方法。

结合使用透明度贴图和纹理动画可以产生很特殊的效果,如闪烁的火炬、植物生长、爆炸和大气效果。

对于使用透明度贴图进行模型渲染,存在一些可选项。透明度混合允许半透明,会在模型边上开启反走样。然而,透明度混合要求在不透明模型渲染完毕后再去渲染透明物体,并且从后到前渲染。一个简单的交叉树是一个反例,其中的渲染顺序无法确定的,因为每个四边形都有一部分在另一个四边形前。尽管理论上可以对模型进行排序然后确定渲染顺序,但通常这样做效率很低。例如,某一区域可能会有上千使用裁切来表示的草地。每个网格模型可能由许多独立的叶子组成,进行精确地排序就会很复杂。

这一问题可以在渲染时使用一些不同的方式来改善。一种方法是使用透明度测试,它是一个根据透明度和阈值来部分裁剪片段的一个过程:

if(texture.a < alphaThreshold) discard;

其中texture.a是查找自纹理的透明度值,参数alphaThreshold是一个用户提供的阈值,用于决定该哪些片段会被剔除。这种二分可视性测试让三角形可以用任何顺序渲染,因为透明片段会被剔除。我们通常想对任何透明度值为0的片段执行这一操作。完全剔除透明片段可以减少着色器进程和合成的开销,同样可以避免将错误的片段设置为可见。对于裁切我们通常将阈值设置的高于0.0,假设为0.5或更高,然后忽略一些片段,不用于进一步的混合。这样可以避免次序颠倒的问题。然而,因为只有两个级别的透明度(完全不透明和完全透明),所以说质量会很低。另一种解决方法是对每个模型执行两个pass,其中一个进行裁剪,会写入深度,另一个进行半透明采样,不进行深度写入。

透明度测试有两个问题,过度缩小和过度放大。当透明度测试使用mipmapping进行时,如果没有进行一些操作的话就会让结果不可信。一个例子见下图:


其中树的叶子比期望的要更透明些。这可以通过一个例子解释。假设我们有一个拥有4个透明度值得一维纹理,(0.0,1.0,1.0,0.0)。使用平均的话,下一个mipmap等级为(0.5,0.5),然后上一等级为(0.5)。现在,假设我们使用\alpha_t=0.75,当放位等级0Mipmap时,我们可以发现4个纹素中的1.5个纹素会通过透明度测试。然而,当访问接下来的两个级别时,因为0.5<0.75,所以会剔除所有的片段。下图是另一个例子:

Castano展示了一个在mipmap创建期间解决问题的方案,对于k级mipmap,覆盖范围c_k定义为:

c_k= \frac {1} {n_k} \sum_i (\alpha(k, i)> \alpha_t)

其中n_k时k级mipmap中纹素的数量,\alpha(k,j)是像素i在k级mipmap储的透明度值,\alpha_t是用户定义的透明度阈值。这里,我们假设\alpha(k,i)>\alpha_t如果为真的话结果是1,否则是0。注意k=0表示最低mipmap等级,即最开始的图片。对于每个mipmap级别,我们会找到一个新的mipmap阈值\alpha_k,这样的c_k会等于c_0。这可以通过一个二分查找来完成。最后,在k级mipmap中的所有纹素的透明度值由\alpha_t/\alpha_k缩放。这一方法使用结果见下图:

Golus在mipmap没有被修改的地方给定了一个变体,不过在着色器中透明度值会随mipmap等级提升而增大。

Wyman和McGuire了一个不同的解法,其中阈值剔除条件变为:

if(texture.a < random()) discard;

随机函数返回一个在[0,1]内的随机值,意味着在平均情况下这会得到正确的结果。例如,如果纹理查找中的透明度值为0.3,片段会有30%的几率被剔除。这是逐像素使用一个采样点的随机透明度的一种方法。实际中,随机函数会使用一个哈希函数进行替代,来使用比较占空间的高频噪声:

float hash2D(x,y){return fract(1.0e4*sin(17.0*x+0.1*y)*(0.1+abs(sin(13.0*y+x))));}

一个三维的哈希方法由上述方法的嵌套来完成,即

float hash3D(x,y,z){return hash2D(hash2D(x,y),z);},

该方法会返回一个[0,1)之间的值。输入到哈希方法中的值是除以屏幕空间的对象空间坐标的导数的对象空间坐标,并使用了一个夹紧操作。对于沿z轴移动,我们需要注意其稳定性,该方法可以和暂存反走样技术结合使用。该技术会随距离渐变,因此如果靠近的话我们不会得到任何随机的效果。该方法的一个优势是每个片段通常是正确的,而Castano的方法会对每个mipmap等级创建单个\alpha_k。然而,该值会随mipmap级别变化,可能会降低质量,并且要求艺术家进行一些创造。

透明度测试在放大时会显示波纹感,可以通过预计算透明度贴图为一个距离域来避免这一问题。

\alpha平均,和类似的特性,透明度适应性反走样,它们将片段的的透明度值转换为像素中覆盖采样点的数量。这一想法类似于纱门透明,但是在子像素级别。想象一下每个像素有四个采样点位置,一个片段覆盖一个像素,但是由于剪切纹理,因此是25%透明度的。\alpha模式让片段变得完全不透明,但它只覆盖四个采样点中的三个。例如,该模式对于重叠草叶的剪切纹理来说非常有用。由于每个绘制的采样点是完全不透明的,最近的叶子会用一种一致的方式沿着边隐藏在其后的叶子。由于透明度混合关闭,因此不需要排序来正确绘制半透明边。

\alpha平均非常适合反走样透明度测试,不过在透明度混合时会显示出粗糙感。例如,两个拥有相同\alpha平均比例的透明度混合片段会使用相同的子像素模式,意味着一个片段会完全覆盖另一个,而不是混合。Golus讨论了使用fwidth()着色器指令来给定锐利边缘的方法,见下图:

对于任何透明度映射的使用,必须要理解二次线性插值会如何影响颜色值。想象一下两个邻近的纹素:rgba(255,0,0,255),红色不透明,和rgba(0,0,0,2),黑色几乎透明。那么它们是如何混合的?简单的插值得到结果(127,0,0,128),结果是半透明浅红色。然而,结果并不完全如此,而是预乘过透明度值的全红色。如果我们在这些透明度值中插值,为了正确的插值,我们需要确保被插值的颜色在插值前已经预乘过的透明度值。例如,想象一下一个几乎透明的邻近纹素,rgba(0,255,0,2),很浅的绿色。如果没有预乘透明度值的话插值会得到结果(127,127,0,128),预乘的邻近纹素是(0,2,0,2),那么插值后的结果是(127,1,0,128)。这样的结果会更合理一些。

忽略二次线性插值给定预乘结果这一点会导致贴纸和剪切物体产生黑边。中间红色结果被管线的剩余部分当作一个预乘颜色,边缘会是黑色。这一效果即使是使用透明度测试也会看见。最佳的策略是在二次线性插值使用前进行预乘。WebGL API提供了这一特性,因为合成对于网页来说非常重要。然而,二次线性插值通常由GPU执行,在纹素值上的操作不能在该操作执行前由着色器完成。图片木能预乘为例如PNG的文件格式,因为这么做会丢失颜色精度。这两个因数结合会在使用透明度映射时默认导致黑边效果。一个通用的应变方法是预处理剪切图片,使用从临近不透明纹素获得的颜色来绘制透明的黑纹素。所有的透明区域通常需要用这种方式重新绘制,这样mipmap级别也可以避免黑边问题。同样需要注意预乘值应该在使用透明度值形成mipmap时使用。

凹凸映射

这一节会描述一类技术,它们统称为凹凸映射。所有的这些方法通常通过修改逐像素着色路径来实现。它们能够给出更多的三维细节,同时不需要添加额外的几何体。

在物体上的细节可以分为3种:覆盖许多像素的宏观特征,涉及很少像素的中央特征,以及比一个像素还要小的微观特征。这些分类不固定,因为观察者可能在动画或交互时在许多不同的距离观察相同的物体。

宏观几何体由顶点和三角形组成,或者其它的几何基本体。当创建一个三维角色时,四肢和头通常是宏观级别的。微观几何体包裹在光照模型中,通常在像素着色器中实现,使用纹理贴图作为参数。使用的光照模型模拟与一个表面的微观几何体的交互,例如,闪亮的物体在微观层面是平滑的,漫反射表面在微观层面是粗糙的。角色的皮肤和衣服因为使用不同的着色器或者至少使用了不同的参数,因此显示不同的材质。

中央几何体描述任何在这两个级别中的事物。它包含使用独立三角形渲染的过于复杂的细节,但对观察者来说细节又足以在几个像素间区分出表面的曲率变化。角色脸上的褶皱,肌肉细节,衣服上的褶皱和接缝,这些都是中央级别的细节。而被称为凹凸映射的技术就是用来构建这些中央级别细节的。它们用某种方式在像素级别修改光照参数,这样的话观察者就可以在基础几何体上看到一些细节,但实际上表面仍是扁平的。不同凹凸映射技术的取决在于它们如何表示细节特性。相关变量包括真实度级别和细节特性的复杂性。例如,艺术家可以在模型上雕刻细节,然后使用软件将这些几何元素转换到一张或多张纹理中,如凹凸纹理。

Blinn介绍了一种在编码中央级别细节到纹理中的方法,他观察到如果在光照时维表面法线提供一个干扰项,那么表面看上去就会有用很小级别的细节。他将描述表面法线干扰的数据存储到数组中。

主要的想法是,不使用纹理来改变光照等式中的颜色组件,而是访问纹理来修改表面法线。表买你的几何法线保持不变,我们只修改在光照等式中使用的法线。这一操作并没有物理上等价,我们改变表面法线,在但几何体层面它仍是光滑的。逐像素使用相同的法线的话,表面就是光滑的,相反,逐像素修改法线就会看起来修改了几何体的表面,但实际上没有变化。

对于凹凸映射,法线必须根据一些引用帧来改变方向。为了这么做,切线帧,或者说切线空间偏移,会在每个顶点存储。这个引用帧被用来将光源变换到表面位置空间来计算扰动法线的效果。通过一个应用了法线贴图的多边形表面,除了顶点法线,我们还存储了切线和双切线矢量。双切线矢量也可称为双法线矢量。

切线和双切线矢量表示法线贴图在对象空间的轴向,因为目的是将灯光转换到与贴图相关的空间。见下图:


这三个矢量,法线n,切线t,双切线b,可以构成一个矩阵:

\left(\begin{matrix}t_x&t_y&t_z&0\\b_x&b_y&b_z&0\\n_x&n_y&n_z&0\\0&0&0&1\end{matrix}\right)

这一矩阵有时被简写为TBN矩阵,将一个灯光的方向从世界空间转换到切线空间。这些矢量并不需要真得彼此垂直,因为法线贴图本身就可能为了适配表面进行了弯曲。然而,一个非正交的偏移会引入歪曲到纹理中,这就意味着需要存储更多信息,可能会造成性能问题,即矩阵之后不能简单的通过转置来进行逆变换。一种节省内存的方法是只在每个顶点存储切线和双切线,然后叉乘计算法线。然而,这一技术只有在矩阵的旋性一致时才有用。通常,一个模型是对称的:飞机、人、文件柜和许多其它的物体。因为纹理会花费大量地内存,它们通常会在对称模型上镜像存储。因此,只有模型一边的纹理会被存储,但纹理映射会在两侧进行。在这种情况下,切线空间的旋性会在两侧不一样,因此无法估计。如果在每个顶点用额外的位存储旋性的话,还是可以避免存储法线信息的。如果这么坐了,这一位会用来对切线和双切线的叉乘结果取反来产生正确的法线。如果切线帧是正交的,也可以将矩阵存储为四元数,可以节省空间,也可以减少逐像素的计算。质量的损失是肯定的,但实际中几乎感受不到。

切线空间的理念对其它算法来说也很重要,下一张会进行讨论,许多算法会基于法线的方向进行。然而,一些材质,如拉丝铝或丝绒还需要知道观察者和光源相对表面的方向。切线空间非常适合用来进行材质表面的朝向。Lengyel和Mittring的文章提供了这一领域的介绍。Schuler展示了一种在像素着色器中飞速计算切线空间基矢量的方法,不需要逐顶点存储预计算过的切线帧。Mikkelsen改善了这一技术,使其不需要任何参数,只需要使用表面位置和高度域来计算扰动法线。然而,这样的技术会比普通的切向空间映射丢失更多的细节,也会导致艺术工作流程的问题。

Blinn方法

Blinn的最初的凹凸贴图方法在纹理的每个纹素中存储两个值,b_ub_v。这两个值与沿uv轴变化的法线的数量相关。即,这些纹理值,通常是二次线性插值的,被用来缩放与法线垂直的两个矢量。这两个矢量被加到法线上来改变方法。这两个值b_ub_v描述了在表面上某点的朝向。见下图:

这种类型的凹凸贴图纹理被称为偏移矢量凹凸贴图或偏移贴图。

另一个表示凹凸的方法是使用高度域来修改表面发现的方向。每个黑白纹理的值表示一个高度,所以在纹理中,白色是高的区域,黑色是低的。见下图:


这是在首次创建和扫描凹凸贴图时使用的格式,由Blinn提出。高度域被用来获得u和v标记值。这是通过获取相邻列的差来获得u的梯度和相邻行的差来获得v的梯度来完成的。一种变体是使用Sobel滤波。

法线映射

一种通用的凹凸映射的技术是法线贴图。算法和结果在数学上都等价Blinn的方法,只是存储的格式和像素着色器的计算发生了改变。

法线贴图会编码映射到[-1,1]范围的(x,y,z),例如,对于8位纹理,x轴的0值代表-1.0,255代表1.0。一个例子见下图:


颜色[128,128,255],是亮蓝色,代表表面的法线[0,0,1]。

法线贴图一开始是由世界空间的法线填充的,实际中很少用到。对于这种类型的映射,很直接:在每个像素,从贴图获得法线,然后直接使用。法线贴图可以定义在模型空间,这样模型可以旋转,法线仍是合法的。然而,两者都要求几何体用某种特定朝向来绑定法线贴图,这样会有一定的限制。

通常,法线会从切线空间获得,即与表面本身相关。这允许表面变形,同时最大化使用法线纹理。切线空间法线贴图也可以很好地压缩,因为z组件可以假设为正向。

法线映射可以被用来提升真实感,见下图:


对比其它的颜色纹理,滤波法线贴图仍是一个难点。通常,法线和光照着色不是线性相关的,所以标准的滤波方法可能会导致走样。想象一下由闪亮白色大理石构建的楼梯,在某些角度,上方和侧方可以获得光照,并反射产生高光。然而,对于楼梯,假设平均法线的角度是45度,这样就会从所有可能的不同的方向产生高光。当使用尖锐高光的凹凸贴图没有用正确的滤波进行渲染,就会在采样点少的地方出现闪烁的高光。

兰伯特表面是一个特殊的例子,其中法线贴图几乎是线性着色的。兰伯特着色几乎是一个点积,是一个线性操作。平均一组法线和与结果执行点积和与平均所有单独的与法线的点积等价:

l\cdot (\frac {\sum^n_{j=1}n_j} {n} = \frac {\sum^n_{j=1}(l\cdot n_j)} {n})

注意平均矢量在使用前并不是标准化的。上述的等式展示了标准滤波和mipmap几乎可以针对兰伯特表面产生正确的结果。因为兰伯特光照等式并不是一个点积而是一个夹紧点积,所以说结果并不是完全正确。夹紧操作让其变得非线性。当扫视光源方向时会让表面看起来较暗,但是实际中通常不会这样。一个警告是一些针对法线贴图使用的压缩方法并不支持非单位长度法线,所以使用非标准法线贴图会让压缩变得很困难。

在非兰伯特表面的例子中,通过滤波输入到光照等式中的输入作为组可以产生更好的结果。

最后,从一个高度图中获得法线贴图这一技术是很有用的。这通过下面的几个式子完成。首先,近似对x和y方向求导,使用中心差完成:

h_x(x,y)=\frac {h(x+1,y)-h(x-1,y)} {2},h_y(x,y)=\frac {h(x,y+1)-h(x,y-1)} {2}

在纹素(x,y)处的非标准法线然后进行计算:

n(x,y)=(-h_x(x,y),-h_x(x,y),1)

水平映射可以帮助提升法线贴图的效果,它让凹凸可以投射阴影到表面上。这通过预计算额外的纹理完成,每个纹理绑定一个沿表面的方向,对每个纹素在该方向存储水平角。

视差映射

凹凸和法线映射的一个问题是凹凸本质上并没有随观察角度改变位置。如果我们沿一个真实的砖墙看去,在某些角度,我们并不会看到砖块间的灰泥层。一个墙面的凹凸贴图永远不会展示这种遮蔽,它只是变换了法线。在每个像素渲染时真得偏移表面位置效果可能会更好。

视察映射的概念由Kaneko于2001年提出,由Welsh润色。视差即观察者移动时,物体的位置会相对应的移动到另一个地方。当观察者移动时,凹凸应该显示出一些高度。视察映射的最关键的理念是通过测试可以被看到的高度来猜测会看到哪一个像素。

对于视差映射,凹凸值被存储在高度域纹理中。当在给定的像素上观察表面时,在该位置上的高度域会被获取,用来偏移纹理坐标来获得表面的不同部分。偏移的数量取决于获取的高度和眼与表面的角度。见下图:


高度域值可以存储在单独的纹理中,也可以整合到其它的纹理的某个通道中。高度域值在被使用来偏移坐标前被缩放和偏移。缩放决定表面会多高,偏移会给定基准平面的高度。给定一个纹理坐标位置p,调整过的高度域h,和一个标准观察矢量,和高度值v_z和水平组件v_{xy},视察调整过的纹理坐标p_{adj}为:

p_{adj}=p+\frac {h\cdot v_{xy}} {v_z}

注意不同于大多数的光照等式,观察矢量位于切线空间中。

如果凹凸的高度改变的很慢的话,这一简单的近似实际上回表现出比较好的效果。邻近的纹素会有相同的高度,所以使用初始位置的高度作为新位置高度的基准这一理念是正确的。然而,该方法会在某些很小的观察角度处失败。当观察矢量沿着表面水平观察时,很小的高度变化会导致大范围的纹理坐标偏移。因为新的位置获取没有来自初始表面位置的高度修正,所以近似错误。

为了改善这一问题,Welsh提出了限制偏移的观点。理念是限制偏移的数量,让其不会比获取的高度要大。等式如下:

p'_{adj}=p+h\cdot v_{xy}

注意这一等式比一开始计算的要快。几何上,插值即高度定义一个位置不能偏移的一个半径,见下图:


在陡峭的角度时,这一等式几乎和一开始的一样,因为v_z几乎为1。在小角度时,偏移会受到限制。视觉上,这会减弱在小角度时的凹凸感。不过视角改变时,问题还是存在。即使是有这些缺点,带偏移限制的视察映射只会消耗几个额外的像素着色器调用,可以得到比法线贴图更好的效果。Shishkovtsov通过在凹凸贴图的法线方向上移动估计的位置来提升视差贴图的阴影效果。

视差遮蔽映射

凹凸映射并不修改基于高度域的纹理坐标,它只随着色法线变换。视察映射提供一个简单的高度域效果的近似,每个像素上尽可能地让其的高度等于邻近的像素。这一近似可能会迅速地失败。凹凸可能永远不会遮蔽其它,或着说产生阴影。我们所希望的是像素上能看到什么。

为了用更好的方法解决这一问题,一些研究人员提出沿观察矢量光线步进直到找到一个交叉点。这一工作可以在像素着色器中完成,其中高度数据可以从纹理中获取。我们将这些方法归为视察映射技术的一个子集。

这些类型的算法被称为视差遮蔽映射(POM)或者浮雕映射方法。核心的理念是首先测试一个固定高度域数字纹理,沿投影矢量采样。在入射角处的观察射线会生成更多的采样点,因此最近的交叉点会被忽略。每个沿射线的三维位置被获取,变换到纹理空间,然后被处理来决定其是否高于或低于某个高度。一旦一个采样点低于高度域,那么其量就在其下,前一个采样点的量就在其上,然后用于找到一个交叉位置。见下图:


位置之后被用于进行着色,使用附着的法线贴图、颜色贴图和其它纹理。多重层高度可以用来产生凸出部分,独立的重叠层和假双面浮雕。高度跟踪方法可以使用来让凹凸面投射纹理到其上。见下图:


在这一课题上存在许多的文献。尽管所有的方法都沿一条射线,他们还是有一些不同。我们可以使用一张简单的纹理来获取高度,但也可以使用一些更高级的数据结构和根查找方法。一些技术可能包括着色器剔除像素或写入到深度缓冲这些操作,可能会损耗性能。下面我们总结一下这些方法,但记住随着GPU发展,会产生最佳的方法的。

在两个常规采样点间决定实际的交叉点的问题是一个根查找问题。实际中,高度域会被当作深度域,四边平面决定表面的上限。这种方式下,表面的初始点会高于高度域。在找到最后一个高点后,第一个点会变为低点。Tatarchuk使用一个正切法的单次迭代来找到一个近似的方法。Policarpo等人使用两个点间的二分查找来逼近一个最近的交叉点。Risser等人通过使用一个正切方法迭代来快速收敛。折中方法是常规的采样可以并行执行,交互方法需要少量的完全纹理访问,但必须等待结果并且执行较慢的依赖纹理获取操作。暴力解法似乎看上去可以执行的不错。

对高度域采样足够的频率是很重要的。McGuire提出了偏移mipmap查找和使用各向异性mipmap来确保针对高频高度域采样的方法,例如钉子和毛发。我们也可以用比法线贴图更高的分辨率来存储高度域纹理。最后,一些渲染系统甚至不存储法线贴图,而是使用一个交叉滤波器来从高度域中飞速获取法线。

另一个提高性能和采样准确率的方法并不会一开始以某一间隔采样高度域,而是试着跳过干扰的空白空间。Donnelly预处理高度域到一个体素中,在每个体素中存储离高度域表面有多远。这种方式下,干涉空间可以快速跳过,不过每个高度域的存储开销更大。Wang等人使用一个五维置换映射方法来存储到任何方向和位置的表面的距离。这允许复杂的曲面、自我投射阴影和其它效果,不过耗费的内存极高。Mehra和Kumar使用方向性距离贴图来实现同样的效果。Dummer介绍了一种方法,Policarpo和Oliverira改进的,称为锥体递进映射。理念是对每个高度域位置存储一个锥体半径。这个半径定义了在光线上的间隔,至少有一个交叉点。这一特性允许快速地沿光线跳过,不会遗漏任何可能的交叉,不过需要耗费性能在纹理读取上。另一个缺点是所需的预计算需要创建一个锥体递进贴图,让这一方法不适合动态改变高度的情况。Schroders和Gulik提出了四叉树浮雕映射技术,是一种层级方法,在遍历时跳过体。Tevs等人使用最大化mipmap来允许在减少预计算开销的同时允许跳过操作。Drobot同样在mipmap中使用了类似四叉树的结构来加速遍历,还展示了一种方法来在不同的高度间混合,其中一种地形类型会转换为另一种。

上述所有方法的一个问题是模型剪影处的光照会失败,即显示出储是的表面光滑外轮廓,见下图:


核心的观念是渲染的三角形定义了哪个像素应该由像素着色器进行计算,而不是会定位在表面的哪个位置。另外,对于曲面,剪影的问题就更明显了。一种由Oliveira和Policarpo提出的方法是使用一个四边形剪影近似。Jeschke等人和Dachsbacher等人给出了一个更通用且更健壮的方法,用于正确处理剪影和曲面。一种由Hirche提出的方法是在每个网格的外围挤出三角形,组成一个棱镜。渲染这些棱镜可以让所有的像素强制进行计算,那么所有的高度域可以正常地显示。这种方法被称为外壳映射,扩展的网格组成一个包裹原始模型的独立外壳。通过在与光线交叉时保持棱镜的非线性自然属性,就有可能渲染出可信的结果,只是会花费更高的性能去计算,效果见下图:


纹理光

纹理可以添加到光源上来增加细节,并且允许使用更复杂的强度分布或聚光灯函数。对于将自身光照限制在锥体内的光源,投影纹理可以被用来操作光强。这允许有形状的聚光灯,组件灯、甚至是滑动投影器效果。见下图:


这些光源通常被称为gobo或cookie光。

对于没有限制在锥体内,但向各个方向发射光线的光源,一个立方体贴图可以被用来操作强度。一维纹理可以被用来定义任意距离衰减函数。与二维角度衰减贴图结合,可以对复杂体积灯光组件使用。一个更通用的可能是使用三维纹理来控制灯光衰减。这允许任意的体积效果,包括光线效果。这一技术是内存密集的。如果光源的效果体式沿三个轴对称的,内存可以通过镜像来减少消耗。

纹理可以添加到任何灯光类型上来开启额外的效果。纹理光允许让艺术家自己控制某些光照属性,可以自由定义要使用的纹理。

相关资源

  • Heckbert在其的研究报告中深入的描述了纹理映射的理论。
  • Szirmay-Kalos和Umenhoffer对于视差遮蔽映射和置换方法进行了调查研究。
  • 更多关于法线表示的信息可以在Cigolle等人和Meyer等人的研究中找到。
  • Advanced Graphics Programming Using OpenGL这本书对于使用纹理映射算法来实现一些视觉效果的方法进行了讲解。
  • 对于程序化纹理生成,可以看Textureing and Modeling: A Procedural Approach一书。
  • Advanced Game Development with Programmable Graphics Hardware一书有许多关于视差遮蔽映射实现的细节。
  • Tatarchuk、Szirmay-Kalos和Umenhoffer的研究同上。
  • 对于程序化纹理,可以看一下Shadertoy这个网站。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345