计算机图形学基础-11-纹理映射

《Fundamentals of Computer Graphics》5th(计算机图形学基础/虎书),中文翻译。

第 11 章 Texture Mapping 纹理映射

在试图复制真实世界的外观时,人们很快意识到几乎没有表面是没有特征的。木材呈现纹理;皮肤展现皱纹;织物显示其编织结构;涂料显示出喷刷或滚筒施工的痕迹。即使是光滑的塑料也有模具成型后的凸起,而光滑的金属则显示出加工过程中的痕迹。曾经无特征的材质很快就会被标记、凹痕、污渍、划痕、指纹和灰尘覆盖。

在计算机图形学中,我们将所有这些现象归为“空间变化的表面属性”,即表面特性在不同位置上发生变化,但并不以有意义的方式改变表面的形状。为了允许这些效果,各种建模和渲染系统提供了一些纹理映射的手段:使用称为纹理图、纹理图像或纹理的图像来存储你想要放置在表面上的细节,并通过数学上的“映射”将图像映射到表面上。

在本文中,“映射”是指第 2.1 节中的意义。

事实证明,一旦将图像映射到表面的机制存在,就有许多不太明显的方式可以使用它,超出了引入表面细节的基本目的。纹理可以用于制作阴影和反射效果,提供照明,甚至定义表面形状。在复杂的交互式程序中,纹理被用来存储各种与图片无关的数据!

本章讨论了使用纹理来表示表面细节、阴影和反射的方法。虽然基本思想很简单,但是几个实际问题会使纹理的使用变得复杂。首先,纹理很容易变形,而设计将纹理映射到表面的函数是具有挑战性的。此外,纹理映射是一种重新采样的过程,就像调整图像大小一样,在第 10 章中我们看到,重新采样很容易引入混叠伪影。使用纹理映射和动画结合在一起很容易产生真正引人注目的混叠伪影,纹理映射系统的很大一部分复杂性都是由于用于驯服这些伪像的抗锯齿措施所造成的。

11.1 查找纹理值

首先,让我们考虑纹理映射的一个简单应用。我们有一个木地板的场景,我们想要地板的漫反射颜色由显示木纹条的图像控制。无论我们使用光线跟踪还是光栅化,计算射线-表面交点或光栅化器生成的片段的颜色的着色代码都需要在着色点处知道纹理的颜色,以将其用作第 5 章中的 Lambertian 着色模型中的漫反射颜色。

为了获得这个颜色,着色器执行纹理查找:它确定与着色点对应的纹理图像坐标系中的位置,并在图像中读取该点处的颜色,从而得到纹理样本。然后将该颜色用于着色,由于每个看到地板的像素都在纹理上不同的位置进行纹理查找,因此图像中会出现不同颜色的图案。代码可能如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
Color texture_lookup(Texture t, float u, float v) {
int i = round(u ⋆ t.width() - 0.5)
int j = round(v ⋆ t.height() - 0.5)
return t.get_pixel(i,j)
}
Color shade_surface_point(Surface s, Point p, Texture t) {
Vector normal = s.get_normal(p)
(u,v) = s.get_texcoord(p)
Color diffuse_color = texture_lookup(u,v)
// compute shading using diffuse_color and normal
// return shading result
}

在这个代码中,着色器询问表面在纹理中查找的位置,我们希望使用纹理进行着色的每个表面都需要能够回答这个查询。这使我们想到了纹理映射的第一个关键要素:我们需要一种函数,将表面映射到纹理上,以便我们可以轻松地为每个像素计算它。这就是纹理坐标函数 (texture coordinate function)(图 11.1),我们称其为分配纹理坐标给表面上每个点。在数学上,它是从表面 SS 到纹理 TT 的映射:

ϕ:ST:(x,y,z)(u,v).\begin{aligned} \phi & : S \rightarrow T \\ & :(x, y, z) \mapsto(u, v) . \end{aligned}

fig11_1.jpg

图 11.1。就像观察投影 ππ 将物体表面上的每个点映射到图像中的点一样,纹理坐标函数 ϕϕ 将物体表面上的每个点映射到纹理图中的点。适当地定义这个函数 ϕϕ 对于所有纹理映射的应用来说是根本的。

纹理坐标函数是一个从表面到纹理的映射,通常称为“纹理空间”的 TT 集合通常只是包含图像的矩形;通常使用单位正方形 (u,v)[0,1]2(u, v) ∈ [0,1]2(在本书中,我们将使用 uuvv 这两个名称作为纹理坐标)。在许多方面,它类似于第 8 章中讨论的观察投影π,本章中称为 ππ,它将场景中表面上的点映射到图像中的点;两者都是 3D 到 2D 的映射,并且都需要用于渲染 —— 一个用于知道从哪里获取纹理值,另一个用于知道在图像中放置着色结果的位置。但也有一些重要的差异:ππ 几乎总是透视或正交投影,而 ϕϕ 可以采用许多形式;对于一个图像,只有一个观察投影,而场景中的每个物体很可能都有完全不同的纹理坐标函数。

当我们的目标是将纹理映射到表面上时,ϕϕ 是从表面到纹理的映射,可能会令人惊讶,但这确实是我们需要的函数。

所以 … 首先你必须学会反向思考?

对于木地板的情况,如果地板恰好在常数 zz 上,且与 xxyy 轴对齐,我们可以使用映射

u=ax;υ=byu=ax; υ=by,

为某些适当选择的比例系数 a 和 b 分配纹理坐标 (u,v)(u,v) 到点 (x,y,z)floor(x,y,z)_{floor},然后使用最接近 (u,v)(u,v) 的纹理像素或 texeltexel 的值作为 (x,y)(x,y) 处的纹理值。通过这种方式,我们渲染了图 11.2 中的图像。

fig11_2.jpg

图 11.2。使用纹理坐标函数直接使用点的 xxyy 坐标对木地板进行纹理映射。

然而,这种方法很有限:如果房间相对于 xxyy 轴呈角度,或者我们想在椅子的弯曲背部上使用木质纹理会怎样?我们需要一些更好的方法来计算表面点的纹理坐标。

最简单的纹理映射形式带来的另一个问题是,当从非常浅的角度渲染高对比度纹理到低分辨率图像时,会出现明显的问题。图 11.3 显示了使用相同方法但具有高对比度网格模式并且朝向地平线的更大平面。您可以看到它包含混叠伪影(前景中的阶梯状,远处的波纹和闪光图案),类似于图像重新采样(第 10 章)时未使用适当滤波器时出现的伪影。虽然需要极端情况才能使这些伪影在印刷在书中的小静态图像中如此明显,在动画中这些图案会移动,并且即使它们更加微妙也会非常令人分心。

fig11_3.jpg

图 11.3。一个大的水平平面,使用与图 11.2 相同的方式进行纹理贴图并显示严重的混叠伪影。

现在我们已经看到了基本纹理映射中的两个主要问题:

  • 定义纹理坐标函数
  • 在不引入太多混叠伪影的情况下查找纹理值。

这两个问题对于所有种类的纹理映射应用都是基本的,并在第 11.2 节和第 11.3 节中进行了讨论。一旦您了解它们以及其中的一些解决方案,您就会理解纹理映射。其余部分只是如何为各种不同目的应用基本纹理机制的讨论,这在第 11.4 节中进行了讨论。

11.2 纹理坐标函数

设计好纹理坐标函数 ϕϕ 是获得良好纹理映射结果的关键要求。您可以将其视为决定如何扭曲平面矩形图像以使其符合要绘制的 3D 表面的形状。或者,您正在轻轻地压扁表面,而不让它皱裂、撕裂或折叠,以便它平放在图像上。有时,这很容易:也许 3D 表面已经是一个平面矩形!在其他情况下,则非常棘手:3D 形状可能非常复杂,例如角色身体的表面。

定义纹理坐标函数的问题对计算机图形学并不新鲜。当设计覆盖大片地球表面的地图时,地图制图师面临着完全相同的问题:从曲面球体到平面地图的映射不可避免地会导致区域、角度和/或距离的失真,从而容易使地图非常误导人。几个世纪以来,已经提出了许多地图投影,都在平衡相同的竞争关注点-最小化各种失真,同时在一块连续的区域内覆盖大面积-这与纹理映射中面临的问题是相同的。

在一些应用程序中(第 11.2.1 节中有一些例子),使用特定的映射方法是很清楚的原因。但在大多数情况下,设计纹理坐标映射是一项平衡竞争关注点的细微任务,熟练的建模人员要花费相当多的精力。

“UV 映射”或“表面参数化”是您可能会遇到的纹理坐标函数的其他名称。

您可以以几乎任何您能想到的方式定义 ϕϕ。但是,有几个竞争目标需要考虑:

  • 双射性:在大多数情况下,您希望 ϕϕ 是双射的(见第 2.1.1 节),因此表面上的每个点都映射到纹理空间内的不同点。如果几个点映射到相同的纹理空间点,则纹理中一个点的值将影响表面上的几个点。在您希望纹理在表面上重复的情况下(例如墙纸或地毯的重复图案),有意识地引入从表面点到纹理点的多对一映射是合理的,但您不希望这种情况发生意外。
  • 大小失真:纹理的比例应在整个表面上近似恒定。也就是说,在表面上彼此距离大约相同的紧密相邻的点应映射到纹理上大约相同距离的点。在 ϕϕ 函数方面, ϕϕ 的导数的幅度变化不应太大。
  • 形状失真:纹理不应该非常扭曲。也就是说,在表面上绘制的小圆圈应该在纹理空间中映射为相当圆形的形状,而不是极度压缩或拉伸的形状。在 ϕϕ 方面, ϕϕ 的导数在不同方向上不应该有太大的差异。
  • 连续性:不应该有太多的接缝:表面上的相邻点应映射到纹理上的相邻点。也就是说, ϕϕ 应该是连续的或具有尽可能少的不连续性。在大多数情况下,某些不连续性是不可避免的,并且我们希望将它们放在不引人注目的位置。

由参数方程(第 2.7.8 节)定义的曲面具有内置的纹理坐标函数选择:只需反转定义表面的函数,并使用表面的两个参数作为纹理坐标。这些纹理坐标可能具有期望的属性,也可能没有,这取决于表面,但它们确实提供了一种映射方式。

但是对于隐式定义或仅由三角形网格定义的表面,我们需要其他方式来定义纹理坐标,而不能依赖于现有的参数化。广义地说,定义纹理坐标的两种方法是从表面点的空间坐标几何计算它们,或者对于网格表面,在顶点处存储纹理坐标值并在表面上进行插值。让我们逐个查看这些选项。

11.2.1 几何确定的坐标

几何确定的纹理坐标用于简单形状或特殊情况,作为快速解决方案,或作为手动调整纹理坐标映射的起点。

我们将通过将图 11.4 中的测试图像映射到表面上来说明各种纹理坐标函数。图像中的数字可让您在渲染后的图像中读出近似的 (u,v)(u,v) 坐标,网格可让您看到映射的畸变程度。

fig11_4.jpg

图 11.4。测试图像。

平面投影

可能从 3D 到 2D 的最简单映射是平行投影-与正交视图使用相同的映射(图 11.5)。我们已经为视图开发的机制(第 8.1 节)可以直接重用以定义纹理坐标:就像正交视图归结为乘以矩阵并丢弃 zz 分量一样,通过平面投影生成纹理坐标可以用简单的矩阵乘法完成:

ϕ(x,y,z)=(u,v) where [uv1]=Mt[xyz1]\begin{array}{c} \phi(x, y, z) = (u, v) \quad \text { where }\left[\begin{array}{l} u \\ v \\ * \\ 1 \end{array}\right] = M_{t}\left[\begin{array}{l} x \\ y \\ z \\ 1 \end{array}\right] \text {, } \end{array}

fig11_5.jpg

图 11.5。如果选择的投影方向大致沿着总体法线,则平面投影对于起始时几乎是平坦的对象或对象部分,可以产生有用的参数化。

这在表面大多数是平坦的,表面法线变化不太多,并且通过取平均法线可以找到良好的投影方向的曲面上效果很好。但是对于任何闭合形状,平面投影将不是单射的:前后两个点将映射到纹理空间中的同一点(图 11.6)。

其中,纹理矩阵 MtM_t 表示仿射变换,星号表示我们不关心第三个坐标的结果。

fig11_6.jpg

图 11.6。在闭合对象上使用平面投影将始终导致非单射的一对多映射,并在投影方向切线处附近产生极端畸变。

通过简单地将透视投影替换为正交投影,我们可以得到投影纹理坐标(图 11.7):

ϕ(x,y,z)=(u~/w,v~/w) where [u~v~w]=Pt[xyz1]\begin{array}{c} \phi(x, y, z)=(\tilde{u} / w, \tilde{v} / w) \quad \text { where }\left[\begin{array}{c} \tilde{u} \\ \tilde{v} \\ * \\ w \end{array}\right]=P_{t}\left[\begin{array}{l} x \\ y \\ z \\ 1 \end{array}\right] \end{array}

fig11_7.jpg

现在,4×44×4 矩阵 PtP_t 表示一个投影(不一定是仿射)变换-也就是说,最后一行可能不是 [0,0,0,1][0,0,0,1]

投影纹理坐标在阴影映射技术中很重要,在第 11.4.4 节中讨论。

球面坐标

对于球体,纬度/经度参数化是熟悉且广泛使用的。它在极点附近具有很多畸变,可能会导致困难,但它确实只沿着一条纬线上覆盖整个球体。

大致呈球形的表面可以使用纹理坐标函数进行参数化,该函数使用径向投影将表面上的点映射到球体上的点:沿着从球体中心穿过表面点的直线,并找到与球体的交点。此交点的球面坐标是您在表面上开始的点的纹理坐标。

另一种说法是,您用球面坐标 (ρ,θ,ϕ)(ρ,θ,ϕ) 表示表面点,然后丢弃 ρρ 坐标,并将 θθϕϕ 各映射到范围 [0,1][0,1]。公式取决于球面坐标约定;使用第 2.7.8 节的约定,

这个和本章其他针对位于原点处的箱子 [11]3[-1,1]^3 中的对象的纹理坐标函数。

ϕ(x,y,z)=([π+atan2(y,x)]/2π,[πacos(z/x)]/π).\begin{array}{c} \phi(x, y, z)=([\pi+\operatorname{atan} 2(y, x)] / 2 \pi,[\pi-\operatorname{acos}(z /\|x\|)] / \pi) . \end{array}

如果从中心点可见整个表面,则球面坐标映射将在极点以外的任何地方都是双射的。它继承了与球体上的纬度-经度映射相同的在极点附近的畸变。图 11.8 显示了一个适合使用球面坐标函数的对象。

fig11_8.jpg

图 11.8。对于这个略微球形的对象,将每个点投影到以物体中心为中心的球体上提供了一个双射映射,它在此处用于放置与地球图像使用相同的映射纹理。请注意,表面远离中心的地方将被放大(表面点在纹理空间中挤在一起),而表面靠近中心的地方将缩小。

柱面坐标

对于更具有列状形状的对象,从轴向外投影到圆柱体上可能比从点向球体投影更好(图 11.9)。类似于球形投影,这相当于将其转换为柱坐标并丢弃半径:

fig11_9.jpg

图 11.9。一个远非球形的花瓶,球形投影会产生很大的畸变(左),而柱形投影在外表面上产生非常好的结果。

ϕ(x,y,z)=(12π[π+atan2(y,x)]/2π,12[1+z])\begin{array}{c} \phi(x, y, z)=\left(\frac{1}{2 \pi}[\pi+\operatorname{atan} 2(y, x)] / 2 \pi, \frac{1}{2}[1+z]\right) \end{array}

立方体贴图

使用球形坐标对球形或类似球体的形状进行参数化会导致在极点附近形状和面积高度扭曲,通常导致可见的伪影揭示出存在两个特殊点,在这些点上与纹理出现问题。一种流行的替代方法是为了更加均匀,代价是具有更多的不连续性。其思想是投射到一个立方体上而不是球体,然后使用六个单独的正方形纹理来表示立方体的六个面。六个正方形纹理的集合称为立方体贴图。这在所有立方体边缘引入不连续性,但保持形状和面积的畸变很低。

计算立方体贴图纹理坐标的成本也比球形坐标更便宜,因为投影到平面上只需要除法-基本上与用于视图的透视投影相同。例如,对于投影到立方体+z 面上的点:

(x,y,z)(xz,yz)\begin{array}{c} (x, y, z) \mapsto\left(\frac{x}{z}, \frac{y}{z}\right) \end{array}

立方体贴图的令人困惑的方面是建立 uuvv 方向在六个面上如何定义的约定。任何约定都可以,但所选择的约定会影响纹理的内容,因此标准化非常重要。因为立方体贴图非常常用于从立方体内部查看的纹理(请参见第 11.4.5 节中的环境映射),通常约定 uuvv 轴取向使得在从内部查看时,uu 是顺时针方向,vv 是逆时针方向。OpenGL 使用的约定是

ϕx(x,y,z)=12[1+(+z,y)/x],ϕ+x(x,y,z)=12[1+(z,y)/x],ϕy(x,y,z)=12[1+(+x,z)/y],ϕ+y(x,y,z)=12[1+(+x,+z)/y],ϕz(x,y,z)=12[1+(x,y)/z],ϕ+z(x,y,z)=12[1+(+x,y)/z].\begin{array}{c} \phi_{-x}(x, y, z) & =\frac{1}{2}[1+(+z,-y) /|x|], \\ \phi_{+x}(x, y, z) & =\frac{1}{2}[1+(-z,-y) /|x|], \\ \phi_{-y}(x, y, z) & =\frac{1}{2}[1+(+x,-z) /|y|], \\ \phi_{+y}(x, y, z) & =\frac{1}{2}[1+(+x,+z) /|y|], \\ \phi_{-z}(x, y, z) & =\frac{1}{2}[1+(-x,-y) /|z|], \\ \phi_{+z}(x, y, z) & =\frac{1}{2}[1+(+x,-y) /|z|] . \end{array}

下标表示每个投影对应的立方体面。例如,对于投影到立方体 x=+1x = +1 面的点使用 ϕxϕ−x。您可以通过查看具有最大绝对值的坐标来确定一个点投影到哪个面:例如,如果 x>y| x |> | y |x>z| x |> | z |,则点投影到 +x+xx-x 面,具体取决于 xx 的符号。

用于立方体贴图的纹理具有六个正方形部分。 (参见图 11.10。)通常它们被打包在单个图像中以用于存储,并排列得好像该立方体已展开。

fig11_10.jpg

图 11.10。表面被投射到立方体贴图中。表面上的点从中心向外投射,每个点都映射到六个面中的一个点。

11.2.2 插值纹理坐标

为了在三角形网格表面上更精细地控制纹理坐标函数,您可以显式存储每个顶点的纹理坐标,并使用重心插值(第 9.1.2 节)在三角形之间进行插值。它的工作方式和您可能在网格上定义的任何其他平滑变化量相同:颜色,法线,甚至是 3D 位置本身。

插值纹理坐标的思想非常简单 —— 但一开始可能有点令人困惑。

让我们看一个单独三角形的例子。图 11.11 显示了用现在已经熟悉的测试图案的一部分映射的三角形纹理。通过查看呈现的三角形上出现的图案,您可以推断出三个顶点的纹理坐标分别为 (0.2,0.2)(0.2,0.2)(0.8,0.2)(0.8,0.2)(0.2,0.8)(0.2,0.8),因为这些是出现在三角形三个角落的点纹理中。就像前面一节中几何确定的映射一样,通过在此处指定每个顶点应该在纹理空间中的位置来控制纹理在表面上的位置。一旦您定位了顶点,三角形间的线性(重心)插值将处理其余部分。

fig11_11.jpg

图 11.11。使用线性插值纹理坐标的单个三角形。(a)在纹理空间中绘制的三角形;(b)在 3D 场景中呈现的三角形。

在图 11.12 中,我们展示了一个常见的可视化整个网格纹理坐标的方式:简单地在纹理空间中绘制三角形,并将顶点定位在它们的纹理坐标上。该可视化显示了哪些三角形使用了纹理的哪些部分,并且对于评估纹理坐标和调试各种纹理映射代码非常方便。

fig11_12.jpg

图 11.12。一个二十面体,在纹理空间中展开,以提供零扭曲但有许多接缝的三角形。

由顶点纹理坐标定义的纹理坐标映射的质量取决于分配给顶点的坐标-也就是说,网格在纹理空间中的布局如何。无论分配什么坐标,只要网格中的三角形共享顶点(第 12.1 节),纹理坐标映射始终是连续的,因为相邻三角形将在它们共享的边缘上的点上达成一致的纹理坐标。但其他所述的良好品质不是那么自动的。单射性意味着三角形在纹理空间中不会重叠-如果它们这样做,就意味着纹理中的某个点将在表面的多个位置显示。

大小失真当纹理空间中的三角形面积与 3D 中的三角形面积成比例时,失真程度很小。例如,如果将角色的面部映射为连续的纹理坐标函数,则经常会发现鼻子被挤压到纹理空间中相对较小的区域中,如图 11.13 所示。尽管鼻子上的三角形比脸颊上的小,但在纹理空间中大小比例更加极端。结果是纹理在鼻子上被放大,因为一个小的纹理区域必须覆盖一个大的表面区域。同样地,将前额与太阳穴进行比较,在 3D 中,三角形的大小相似,但在纹理空间中,太阳穴周围的三角形较大,导致纹理在那里看起来较小。

fig11_13.jpg

图 11.13。面部模型,纹理坐标被分配以实现相对较低的形状失真,但仍显示出中等程度的面积失真。

同样地,在 3D 和纹理空间中三角形形状相似时,形状扭曲较小。例如,图 11.15 中的球体在极点附近具有非常大的形状扭曲。

11.2.3 平铺,包裹模式和纹理变换

允许纹理坐标超出纹理图像的范围通常很有用。有时,这是一个细节:在纹理坐标计算中舍入误差可能导致落在纹理边界上的顶点略微超出,而此时纹理映射机制不应该失败。但它也可以是建模工具。

如果纹理只应覆盖表面的一部分,但纹理坐标已经设置好将整个表面映射到单位正方形,则一种选择是准备一个基本为空的纹理图像,并在一个小区域中包含内容。但那可能需要一个非常高分辨率的纹理图像才能得到足够详细的相关区域信息。另一种替代方案是将所有纹理坐标放大,以便它们覆盖更大的范围 —— [4.5,5.5]×[4.5,5.5][−4.5,5.5]×[−4.5,5.5],例如将单位正方形放置在表面中心的十分之一处。

对于这种情况,在纹理图像覆盖的单位正方形区域之外进行的纹理查找应返回恒定的背景颜色。一种方法是设置一个在单位正方形之外返回的背景颜色,以便在纹理查找之外使用。如果纹理图像已经具有恒定的背景颜色(例如白色背景上的标志),另一种自动将此背景扩展到平面上的方法是安排在单位正方形之外的查找返回纹理图像边缘上最接近点的颜色,可以通过将 uuvv 坐标夹紧为图像中第一个像素到最后一个像素范围内来实现。

有时,我们想要重复出现的图案,例如棋盘、瓷砖地板或砖墙。如果图案在矩形网格上重复出现,使用许多相同数据的图像会浪费资源。相反,我们可以使用环绕索引处理纹理图像之外的纹理查找-当查找点超出纹理图像的右边缘时,它会环绕到左边缘。这可以使用像素坐标的整数余数运算非常简单地处理。

1
2
3
4
5
6
7
8
9
10
11
Color texture_lookup_wrap(Texture t, float u, float v) {
int i = round(u ⋆ t.width() - 0.5)
int j = round(v ⋆ t.height() - 0.5)
return t.get_pixel(i % t.width(), j % t.height())
}
Color texture_lookup_wrap(Texture t, float u, float v) {
int i = round(u ⋆ t.width() - 0.5)
int j = round(v ⋆ t.height() - 0.5)
return t.get_pixel(max(0, min(i, t.width()-1)),
(max(0, min(j, t.height()-1))))
}

处理越界查找的两种方法之间的选择是从一个包含平铺、夹紧和通常是两者组合或变体的列表中选择一种包装模式来指定的。使用包装模式,我们可以自由地将纹理视为在无限 2D 平面上为任何点返回颜色的函数(图 11.14)。当我们使用图像来指定纹理时,这些模式描述了有限的图像数据应如何用于定义此函数。在第 11.5 节中,我们将看到,过程纹理可以自然地延伸到无限平面,因为它们不受有限图像数据的限制。由于两者在逻辑上都是无限的,因此这两种类型的纹理是可互换的。

当调整纹理的比例和位置时,通过对纹理坐标施加矩阵变换而不是实际更改生成纹理坐标的函数或存储在网格顶点处的纹理坐标值,可以方便地进行处理:

ϕ(x)=MTϕmodel (x)\begin{array}{c} \phi(\mathbf{x})=\mathbf{M}_{T} \phi_{\text {model }}(\mathbf{x}) \end{array}

其中,ϕmodelϕ_{model} 是提供给模型的纹理坐标函数,MTM_T 是一个 3×33×3 矩阵,使用齐次坐标表示 2D 纹理坐标的仿射或投影变换。大多数使用纹理映射的渲染器都支持这种变换,有时仅限于缩放和/或平移。

fig11_14.jpg

图 11.14。通过包装纹理坐标平铺在纹理空间上的木地板纹理。

11.2.4 连续性与接缝

尽管纹理坐标函数具有低失真和连续性等良好属性,但不连续往往是不可避免的。对于任何闭合的 3D 表面,拓扑学的基本结果是不存在将整个表面映射到纹理图像中的连续双射函数。必须做出一些妥协,并通过引入接缝-即在表面上纹理坐标突然改变的曲线-使其他地方失真较小。许多上面讨论的几何确定的映射已经包含了接缝:在球面和圆柱面坐标中,接缝位于由 atan2 计算的角度从 πππ 跳转的位置,在立方体贴图中,接缝沿着立方体边缘,其中映射在六个正方形纹理之间切换。

在插值纹理坐标中,接缝需要特别考虑,因为它们不会自然发生。我们之前观察到,在共享顶点的网格上,插值的纹理坐标会自动连续-共享纹理坐标保证了这一点。但是,这意味着如果一个三角形跨越一个接缝,其中一些顶点在一侧,另一些在另一侧,插值机制将欣然提供连续的映射,但很可能会高度扭曲或折叠,以至于不是单射性的。图 11.15 展示了这个问题在用球面坐标映射的地球上的情况。例如,在地球底部有一个三角形,其一个顶点位于新西兰南岛的尖端,另一个顶点位于北岛东北方约 400 公里的太平洋中。一个明智的飞行员从这些点之间飞行会经过新西兰,但是路径起始于 167° E 经度 (+167),结束于 179° W(即经度-179),因此线性插值会选择一条途径在途中穿越南美洲的路线。这导致整张地图的反向副本被压缩到跨越 180 度经线的三角形带中!解决方案是用等效的 181° E 经度标记第二个顶点,但这只会将问题推迟到下一个三角形。

fig11_15.jpg

图 11.15。多边形地球:左图中所有的共享顶点,纹理坐标函数是连续的,但由于纹理坐标从接近 180 度的经度插值到接近 -180 度的经度,所以在穿越第 180 度子午线的三角形上必然会存在问题。右图中一些顶点被复制,具有相同的 3D 位置,但经度上相差正好 360 度,因此纹理坐标在经线上进行插值,而不是在整张地图上进行插值。

创建一个干净的过渡的唯一方法是避免在接缝处共享纹理坐标:穿越新西兰的三角形需要插值到经度 +181,太平洋中的下一个三角形需要从经度 -179 开始继续。为此,在接缝处复制顶点:对于每个顶点,我们添加一个具有等效经度的第二个顶点,相差 360 度,并且接缝两侧的三角形使用不同的顶点。这个解决方案在图 11.15 的右半部分中展示,其中纹理空间最左边和最右边的顶点是重复的,具有相同的 3D 位置。

11.2.5 渲染系统中的纹理坐标

纹理在各种渲染系统中都被使用,尽管基本原理是相同的,但对于光线跟踪和光栅化系统而言,细节有所不同。

纹理坐标是正在渲染的模型的一部分,场景描述需要包含足够的信息来定义它们。大多数情况下,这意味着将纹理坐标存储为所有将用于纹理的三角形网格的顶点属性。如果渲染系统直接支持除网格之外的几何图元,则这些图元通常具有预定义的纹理坐标(例如,球体上的纬度 - 经度坐标),每种图元类型可能有不同的映射方案。

在光线跟踪渲染器中,支持射线交点的每种表面必须能够计算出交点处的纹理坐标,而不仅仅是交点和表面法线。与交点的其他信息一样,纹理坐标可以存储在命中记录中(见第 4.4.3 节)。在三角形网格表示几何体的常见情况下,射线-三角形交点代码将通过从存储在顶点处的纹理坐标进行重心插值来计算纹理坐标,并且对于其他类型的几何体,交点代码必须直接计算纹理坐标。

在基于光栅化的系统中,三角形通常是唯一支持的几何类型,因此所有表面都必须转换为这种形式。纹理坐标可以随模型读取(常见情况),或者对于从代码生成的三角形网格,它们可以在创建网格时计算和存储。另外,对于可以从其他顶点数据计算出纹理坐标的纹理坐标(例如,从 3D 位置计算出纹理坐标的情况),纹理坐标也可以在顶点着色器中计算,并传递给光栅化器。然后由光栅化器插值纹理坐标,以便每个片段着色器调用具有其片段所需的适当纹理坐标。

11.3 纹理查找的抗锯齿技术

纹理映射的第二个基本问题是抗锯齿。渲染纹理映射的图像是一个采样过程:将纹理映射到表面上,然后将表面投影到图像中,在图像平面上产生一个 2D 函数,我们在像素上对其进行采样。正如我们在第 10 章中看到的那样,使用点采样会在图像包含细节或锐利边缘时产生混叠伪影 —— 而由于纹理的整个目的就是引入细节,它们成为类似于我们在图 11.3 中看到的混叠问题的主要源头。

现在回顾一下第 10 章前半部分的内容是一个好主意。

与线或三角形的抗锯齿光栅化(第 9.3 节)、抗锯齿光线跟踪或降采样图像(第 10.4 节)一样,解决方案是使每个像素不是一个点采样,而是一个区域平均值,该区域大小与像素相似。使用用于抗锯齿光栅化和光线跟踪的超级采样方法,通过足够多的样本,可以在不更改纹理映射机制的情况下获得出色的结果:在像素区域内的许多样本将落在纹理映射的不同位置,并对使用不同纹理查找计算的阴影结果进行平均,是一种准确近似像素覆盖的图像的平均颜色的方法。但是,具有详细纹理的情况下需要非常多的样本才能获得良好的结果,这会很慢。在存在表面纹理的情况下高效地计算这个区域平均值,是纹理抗锯齿中的第一个关键主题。

纹理图像通常由光栅图像定义,因此还需要考虑重建问题,就像对于上采样图像(第 10.4 节)一样。解决方案也适用于纹理:使用重建滤波器在纹素之间插值。

我们在以下章节中详细讨论每个主题。

11.3.1 像素的足迹

使纹理抗锯齿比其他种类的抗锯齿更复杂的是,渲染图像和纹理之间的关系不断变化。每个像素值应该计算为像素在图像中所属区域的平均颜色,并且在通常情况下,如果像素正在查看单个表面,则对表面上的一个区域进行平均处理。如果表面颜色来自纹理,则这反过来相当于在纹理的对应部分上进行平均处理,称为像素的纹理空间足迹 (texture space footprint)。图 11.16 说明了正方形区域(可能是低分辨率图像中的像素区域)的足迹如何映射到地面纹理空间中非常不同大小和形状的区域。

fig11_16.jpg

图 11.16。图像中相同大小的正方形区域在纹理空间中的足迹大小和形状因图像而异。

回想一下纹理渲染涉及的三个空间:将 3D 点映射到图像的投影π以及将 3D 点映射到纹理空间的纹理坐标函数 ϕϕ。为了处理像素足迹,我们需要了解这两个映射的组合:首先沿着反向 ππ 从图像到表面,然后沿着正向 ϕϕ 进行跟踪。这个组合 ψ=ϕπ1ψ = ϕ ∘ π^{-1} 决定像素足迹:像素的足迹是该像素在图像的正方形区域在映射 ψψ 下的图像。

纹理抗锯齿中的核心问题是计算像素足迹上纹理的平均值。要在一般情况下准确地做到这一点可能是一项相当复杂的工作:对于具有复杂表面形状的远离物体,足迹可能是一个复杂的形状,覆盖大面积或可能是几个不连续的区域,在纹理空间中。但是,在典型情况下,像素落在被映射到纹理上的单个区域中的表面的光滑区域中。

由于 ψψ 包含从图像到表面和从表面到纹理的映射,因此足迹的大小和形状取决于视图情况和纹理坐标函数。当一个表面靠近相机时,像素足迹会变小;当同样的表面远离时,足迹会变大。当表面以斜角观察时,像素在表面上的足迹被拉长,这通常意味着在纹理空间中也会被拉长。即使在固定视图下,纹理坐标函数也可能导致足迹的变化:如果它扭曲了区域,则足迹的大小将有所变化,而如果它扭曲了形状,则它们甚至在正面观察表面时也可能被拉长。

然而,为了找到计算抗锯齿查找的高效算法,需要进行一些实质性的近似。当一个函数是平滑的时,线性逼近通常是有用的。在纹理抗锯齿的情况下,这意味着将从图像空间到纹理空间的映射 ψψ 近似为从 2D 到 2D 的线性映射:

在数学家的术语中,我们对函数ψ进行了一项一次泰勒级数逼近。

ψ(x)=ψ(x0)+J(xx0)ψ(x)=ψ(x_0)+ J(x−x_0)

其中 二乘二矩阵 J 是对ψ的导数的某种近似。它有四个条目,如果我们将图像空间位置表示为 x=(x,y)x = (x,y),将纹理空间位置表示为 u=(u,v)u = (u,v),则

J=[dudxdudydvdxdvdy]\begin{array}{c} \mathbf{J}=\left[\begin{array}{ll} \frac{d u}{d x} & \frac{d u}{d y} \\ \frac{d v}{d x} & \frac{d v}{d y} \end{array}\right] \end{array}

其中四个导数描述了当我们改变 xxyy 时,在图像中看到点 (uv)(u,v) 如何发生变化。

fig11_17.jpg

图 11.17。可以使用从 xy(x,y)uv(u,v) 的映射的导数进行纹理空间像素足迹的近似。相对于 xxyy 的偏导数与 xxyy 等值线的图像(蓝色)平行,并跨越一个平行四边形(用橙色阴影表示),该平行四边形近似了精确足迹的曲线形状(用黑线轮廓表示)。

这个近似的几何解释(图 11.17)是说,在纹理空间中,以 ψ(x)ψ(x) 为中心且边缘平行于向量 ux=(du/dx,dv/dx)u_x = (du/dx,dv/dx)uy=(du/dy,dv/dy)u_y = (du/dy,dv/dy) 的平行四边形会近似地映射到在图像中以 xx 为中心的单位大小的正方形像素区域。

导数矩阵 J\bold J 很有用,因为它告诉我们整个图像中(近似的)纹理空间足迹变化的全貌。大小更大的导数表明更大的纹理空间足迹,而导数向量 uxu_xuyu_y 之间的关系则表示其形状。当它们正交且长度相同时,足迹是正方形,当它们变得倾斜和/或长度非常不同时,足迹变得拉长。

这里使用盒式滤波器对图像进行采样。某些系统使用高斯像素滤波器,它在纹理空间中变成一个椭圆高斯;这就是椭圆加权平均(EWA)。

现在,我们已经达到了通常被认为是“正确答案”的问题形式:特定图像空间位置处的滤波纹理采样应该是在该点的纹理坐标导数定义的平行四边形形状足迹上纹理映射的平均值。这已经包含了一些假设,即从图像到纹理的映射是光滑的,但它对于出色的图像质量足够准确。然而,这个平行四边形面积平均已经太昂贵,无法精确计算,因此使用各种近似方法。纹理抗锯齿方法在近似此查找时进行速度/质量权衡。我们将在以下部分讨论这些内容。

11.3.2 重建

当足迹小于纹素大小时,我们在将纹理映射到图像时放大了纹理。这种情况类似于对图像进行上采样,主要考虑是在纹素之间进行插值,以产生一个平滑的图像,在其中纹素网格不明显。就像图像上采样一样,这个平滑过程由重建滤波器定义,用于在纹理空间中的任意位置计算纹理采样。(见图 11.18。)

fig11_18.jpg

图 11.18。纹理过滤的主要问题随着足迹大小而变化。对于小足迹(左边),需要在像素之间进行插值,以避免块状伪影;对于大足迹,挑战是有效地找到许多像素的平均值。

考虑与图像重采样基本相同,但有一个重要的区别。在图像重采样中,任务是计算正则网格上的输出样本,这种规则性使得在可分离重建滤波器的情况下能够进行重要的优化。在纹理过滤中,查找模式不是规则的,并且必须单独计算样本。这意味着使用大型高质量的重建滤波器非常昂贵,在此情况下通常用于纹理的最高质量滤波器是双线性插值。

双线性插值的纹理采样计算与使用双线性插值上采样图像中的一个像素相同。首先,我们用(实数值)纹素坐标表示纹理空间采样点,然后读取四个相邻纹素的值并将它们取平均值。纹理通常在单位正方形上参数化,纹素的位置方式与任何图像中的像素相同,在 uu 方向上间隔 1/nu1 / n_u,在 vv 方向上间隔 1/nv1 / n_v,纹素 (0,0)(0,0) 在边缘处位置与对称中心的一半。 (有关完整解释,请参见第 10 章。)

1
2
3
4
5
6
7
8
9
10
Color tex_sample_bilinear(Texture t, float u, float v) {
u_p = u ⋆ t.width - 0.5
v_p = v ⋆ t.height - 0.5
iu0 = floor(u_p); iu1 = iu0 + 1
iv0 = floor(v_p); iv1 = iv0 + 1
a_u = (iu1 - u_p); b_u = 1 - a_u
a_v = (iv1 - v_p); b_v = 1 - a_v
return a_u ⋆ a_v ⋆ t[iu0][iv0] + a_u ⋆ b_v ⋆ t[iu0][iv1] +
b_u ⋆ a_v ⋆ t[iu1][iv0] + b_u ⋆ b_v ⋆ t[iu1][iv1]
}

在许多系统中,这个操作成为了一个重要的性能瓶颈,主要是因为从纹理数据中获取四个纹素值涉及到内存延迟。纹理采样点的模式是不规则的,因为从图像到纹理空间的映射是任意的,但通常是相干的,因为附近的图像点往往映射到附近可能读取相同纹素的纹理点。出于这个原因,高性能系统有专门用于纹理采样的硬件,处理插值并管理最近使用的纹理数据的缓存,以使从存储纹理数据的内存中读取数据的次数最小化。

阅读第 10 章后,您可能会抱怨线性插值对于一些苛刻的应用程序而言可能不够平滑。然而,它总是可以通过使用更好的滤波器将纹理重新采样到稍高的分辨率,以使纹理足够平滑,从而使双线性插值效果良好。

11.3.3 Mipmapping

只做好插值工作还不足以应对纹理被放大的情况:当像素足迹相对于纹素间距很小时。当像素足迹覆盖多个纹素时,良好的抗锯齿需要计算许多纹素的平均值以平滑信号,以便可以安全地进行采样。

一个非常精确的方法是找到足迹内的所有纹素并将它们相加来计算平均纹理值。然而,当足迹很大时,这可能非常昂贵——仅为单个查找可能需要读取许多千个纹素。一种更好的方法是预先计算和存储不同大小和位置的各种区域中的纹理平均值。

“MIP”这个名字代表了拉丁短语 multum in parvomultum \space in \space parvo,意为“小空间中的大量”。

这个想法非常流行,被称为“MIP 映射”或仅仅是 mipmapping。mipmap 是一系列纹理,所有纹理都包含相同的图像,但分辨率越来越低。原始的全分辨率纹理图像被称为 mipmap 的基本级别 (base level) 或级别 0,级别 1 通过在每个维度上将该图像按 2 的因子下采样生成,导致具有四分之一纹素数量的图像。该图像中的纹素大致上是级别 0 图像中 2x2 纹素大小的正方形区域的平均值。

可以继续进行此过程以定义所需的许多 mipmap 级别:级别 kk 的图像是通过将级别 k1k-1 的图像进行两次下采样得到的。级别 k 的纹素对应于原始纹理中 2k×2k2^k \times 2^k 纹素大小的正方形区域。例如,从 1024×10241024×1024 的纹理图像开始,我们可以生成一个具有 11 个级别的 mipmap:级别 0 为 1024×10241024×1024;级别 1 为 512×512512×512,以此类推,直到级别 10 只有一个纹素。这种结构使用了一系列具有相同内容的图像,其采样率越来越低,被称为图像金字塔 (image pyramid),基于视觉隐喻,所有较小的图像都叠放在原始图像上方。

11.3.4 基本的纹理滤波与 Mipmaps

有了 mipmap 或图像金字塔,纹理过滤可以比逐个访问许多纹素更高效地完成。当我们需要对大面积进行平均的纹理值时,我们只需使用 mipmap 的更高级别的值,这些级别已经是图像大区域的平均值。最简单和最快的方法是从 mipmap 中查找单个值,选择级别,以使在该级别上覆盖纹素的大小大致相同于像素足迹的整体大小。当然,像素足迹的形状可能与纹素所代表的(始终为正方形)区域非常不同,我们可以期望这会产生一些伪影。

暂且先不考虑像素足迹具有延长形状的问题,假设足迹是宽度为 D 的正方形,根据全分辨率纹理中的纹素来测量。应该采样哪个 mipmap 级别呢?由于级别 kk 的纹素覆盖宽度为 2k2k 的正方形区域,因此选择 kk 使得

2kD2^k≈D

我们令 k=log2Dk = \log_2 D。当然,这将在大多数情况下给出非整数值的 kk,并且我们仅存储整数级别的 mipmap 图像。两种可能的解决方案是仅为 kk 最接近的整数查找值(高效但产生级别之间的突变无缝连接)或查找 kk 两个最近整数的值并线性插值值(工作量翻倍,但更平滑)。

在实际编写采样 mipmap 算法之前,我们必须决定如何在足迹不是正方形时选择“宽度”D。一些可能的选择可能是使用面积的平方根或找到足迹的最长轴并将其称为宽度。一个易于计算的实用妥协是使用最长边的长度:

D=max{ux,uy}D = \bold {max}\{ ||ux||, ||uy|| \}

1
2
3
4
5
6
7
8
9
10
Color mipmap_sample_trilinear(Texture mip[], float u, float v,
matrix J) {
D = max_column_norm(J)
k = log2(D)
k0 = floor(k); k1 = k0 + 1
a = k1 - k; b = 1 - a
c0 = tex_sample_bilinear(mip[k0], u, v)
c1 = tex_sample_bilinear(mip[k1], u, v)
return a ⋆ c0 + b ⋆ c1
}

基本 mipmapping 能够有效地消除锯齿,但由于它无法处理延长或各向异性的像素足迹,在浅角度 (anisotropic) 下查看表面时性能不佳。这最常见于代表观众所站立的表面的大平面上。远离的地板点以非常陡峭的角度被观察到,导致非常各向异性的足迹,使 mipmapping 用更大的正方形区域进行近似。结果图像在水平方向上会显示模糊。

11.3.5 各向异性过滤

使用多个查找来近似更好地处理延长足迹。想法是基于足迹的最短轴而不是最大轴选择 mipmap 级别,然后沿着长轴间隔平均一些查找(参见图 11.19)。

fig11_19.jpg

图 11.19. 使用三种不同策略(仅使用最近邻插值进行单点采样;使用 mipmap 金字塔在纹理中平均一个正方形区域;使用来自 mipmap 的多个采样来平均纹理中的各向异性区域)对具有挑战性的测试场景进行抗锯齿处理(参考图像显示详细结构,左侧)。

11.4 纹理映射的应用

一旦你理解了为表面定义纹理坐标以及查找纹理值的机制,这个机制具有许多用途。在本节中,我们概述了一些最重要的纹理映射技术,但纹理是一种非常通用的工具,其应用仅受程序员的想象力限制。

11.4.1 控制着色参数

纹理映射的最基本用途是通过使着色计算中使用的漫反射颜色——无论是在光线追踪器中还是在片段着色器中——依赖于从纹理中查找的值,引入颜色变化。可以使用带纹理的漫反射分量来贴花、涂装装饰或在表面上印刷文本,并且它还可以模拟材料颜色的变化,例如木材或石材。

然而,并不限制我们仅仅对漫反射颜色进行变化。任何其他参数,例如高光反射率或高光粗糙度,也可以使用纹理映射。例如,贴有透明胶带的纸板盒子可能在漫反射颜色相同的情况下,在胶带贴附处比其他地方更光泽,具有更高的高光反射率和较低的粗糙度。在许多情况下,不同参数的映射是相关的:例如,印有标志的白色光滑陶瓷杯可能在印刷处更加粗糙和暗淡(图 11.20),而用金属油墨印刷标题的书籍可能会同时改变漫反射颜色、高光颜色和粗糙度。

fig11_20.jpg

图 11.20. 通过反向复制漫反射颜色纹理来控制陶瓷杯子的高光粗糙度。

11.4.2 法线贴图和凹凸贴图

另一个对着色很重要的量是表面法线。使用插值法线(第 9.2 节),我们知道着色法线不必与底层表面的几何法线相同。法线映射利用了这一点,通过使着色法线依赖于从纹理映射中读取的值来实现。最简单的方法是只需将法线存储在纹理中,每个纹素存储三个数字,它们被解释为法线向量的三个坐标,而不是作为颜色的三个分量。

但在使用法线贴图之前,我们需要知道从地图中读取的法线所表示的坐标系。在物体空间直接存储法线,在表示表面几何形状本身的相同坐标系中,最简单:从地图中读取的法线可以像表面本身报告的法线一样使用:在大多数情况下,它需要转换为世界空间用于光照计算,就像来自几何体的法线一样。

然而,存储在对象空间中的法线贴图固有地与表面几何形状相关联,即使对于法线贴图没有影响的情况,为了重现几何法线的结果,法线贴图的内容也必须跟踪表面的方向。此外,如果表面将发生变形,以使得几何法线发生变化,则无法使用对象空间的法线贴图,因为它将继续提供相同的着色法线。

解决方法是为法线定义一个附加到表面的坐标系。可以基于表面的切线空间(参见第 2.7 节)定义这样的坐标系:选择一对切向量并使用它们来定义一个正交基(第 2.4.5 节)。纹理坐标函数本身提供了一种选择一对切向量的有用方法:使用沿着常数 uuvv 线的切向方向。这些切向通常不是正交的,但是我们可以使用第 2.4.7 节中的过程来“调整”正交基,或者可以使用表面法线和一个切向量来定义它。

当法线在此基础上表示时,它们变化较小;由于它们大多指向光滑表面法线的方向附近,它们将靠近法线贴图中的矢量 (0,01)T(0,0,1)^T

法线贴图从哪里来?通常它们是从更详细的模型计算而来,平滑表面只是一个近似;其他时候,它们可以直接从真实表面测量得到。它们也可以作为建模过程的一部分进行编写;在这种情况下,使用凹凸贴图 (bump map) 间接指定法线通常很好。其想法是凹凸贴图是一个高度场:给出详细表面在平滑表面上的局部高度的函数。值高(如果将其显示为图像,则地图看起来很亮)的地方,表面凸出了平滑表面之外;值低(地图看起来很暗的地方),表面下降到平滑表面以下。例如,凹凸贴图中的窄暗线是划痕,或者小白点是凸起。

从凹凸贴图派生法线贴图很简单:法线贴图(在切向框架中表示)是凹凸贴图的导数。

图 11.21 展示了使用纹理映射创建木纹颜色,并模拟由于涂层渗透到木材更多孔隙部分而导致的表面粗糙度增加,以及使用凹凸贴图创建不完美的涂层和板之间的间隙,从而制作逼真的木地板。

fig11_21.jpg

图 11.21. 使用纹理映射控制阴影的木地板渲染。 (a) 只有漫反射颜色受到纹理映射的调制。(b) 镜面粗糙度也受到第二个纹理映射的调制。© 表面法线由凹凸贴图修改。

11.4.3 位移映射

法线贴图的一个问题是它们实际上并没有改变表面;它们只是一种着色技巧。当法线贴图所暗示的几何形状应该在 3D 中产生明显的效果时,这一点变得显而易见。在静止图像中,通常首先注意到的问题是物体的轮廓仍然平滑,尽管内部出现了凹凸。在动画中,缺乏视差揭示了这样一个事实:不论多么令人信服,这些凸起实际上只是“绘制”在表面上。

但是,纹理不仅可以用于着色:它们还可以用于改变几何形状。位移映射就是这个想法的最简单版本之一。概念与凹凸贴图相同:给出高于“平均地形”的高度的标量(单通道)映射。但是效果不同。位移映射实际上改变表面,将每个点沿着光滑表面法线移动到一个新位置。在使用平滑的几何形状时,法线大致相同,但表面却不同。

实现位移映射的最常见方法是使用大量小三角形对平滑表面进行细分,然后使用位移映射位移生成网格的顶点。在图形管道中,这可以在顶点阶段使用纹理查找来完成,并且对地形特别方便。

11.4.4 阴影映射

阴影是场景中物体关系的重要线索,正如我们所看到的,在光线追踪图像中很容易包含它们。 然而,如何在光栅化渲染中获得阴影并不明显,因为表面被单独地一个接一个地考虑。 阴影映射是一种利用纹理映射机制从点光源获取阴影的技术。

阴影映射的思想是表示由点光源照亮的空间体积。想象一个类似聚光灯或视频投影仪的光源,它从点发出光线,并照亮有限范围内的方向。被照亮的体积-如果将手放在那里,您将在哪些点上看到光线-是连接每条离开该点的射线到最近表面点的线段的并集。

有趣的是,这个体积与透视摄像机定位于光源相同的点可见的体积相同:当且仅当从光源位置可见时,点才受到光源的照明。 在两种情况下,需要评估场景中点的可见性:对于可见性,我们需要知道片元是否对摄像机可见,以知道是否在图像中绘制它;对于阴影,我们需要知道片元是否对光源可见,以知道它是否被该光源照亮。 (见 图 11.22)。

fig11_22.jpg

图 11.22. (a) 点光源照亮的空间区域。(b) 由 10 个像素宽的阴影映射近似的该区域。

在两种情况下,解决方案都是相同的:深度图告诉沿着一堆射线最近表面的距离。在可见性情况下,这是 zz 缓冲器(第 9.2.3 节),而对于阴影情况,则称为阴影映射。在两种情况下,通过将新片元的深度与映射中存储的深度进行比较来评估可见性,如果其深度大于最近可见表面的深度,则从投影点隐藏表面(遮挡或阴影)。不同之处在于,zz 缓冲器用于跟踪到目前为止所看到的最近表面,并在渲染过程中更新,而阴影映射告诉整个场景中最近表面的距离。

阴影映射是提前单独进行渲染通道计算的:只需像往常一样将整个场景光栅化,并保留结果深度图即可(无需计算像素值)。然后,使用阴影映射,执行普通的渲染通道,当需要知道片元是否对光源可见时,将其位置投影到阴影映射中(使用第一次渲染阴影映射时使用的相同透视投影),并将查找值 dmapdmap 与实际距离 dd 进行比较。如果二者距离相等,则片元点被照亮;如果 d>dmapd > dmap,则意味着有不同的表面更靠近光源,因此被遮蔽或阴影。

“如果距离相等”这个短语应该在你的脑海中引起一些警惕:由于涉及的所有数量都是具有有限精度的近似值,我们不能期望它们完全相同。对于可见点,ddmapd ≈ dmap,但有时 d 会稍微大一些,有时则小一些。因此,需要一定的容忍度:如果 ddmap<ϵd - dmap < ϵ,则认为点被照亮。这个容差 ϵϵ 称为阴影偏移。

在阴影映射中查找时,插值深度值记录在映射中并没有太多意义。这可能会导致在平滑区域中更准确的深度(需要较少的阴影偏移),但会在阴影边界附近造成更大的问题,因为深度值会突然改变。因此,在阴影映射中进行纹理查找时,使用最邻近重建。为了减少混叠现象,可以使用多个采样,将 1 或 0 的阴影结果(而不是深度)进行平均;这被称为百分比更接近过滤。

11.4.5 环境贴图

就像纹理很方便地介绍了表面着色中的细节,而不必向模型添加更多细节一样,纹理也可以用于在不必建模复杂的光源几何形状的情况下,介绍照明中的细节。当光源远离观察中的物体大小时,从点到点的照明变化非常小。方便的做法是假设照明仅取决于您查看的方向,并且对于场景中的所有点都相同,然后使用环境贴图来表示照明对方向的依赖关系。

这个环境贴图的思想是,在 3D 的方向上定义一个函数,它是单位球上的函数,因此可以使用纹理映射以完全相同的方式表示,就像我们可能在球形物体上表示颜色变化一样。我们不是根据表面点的三维坐标计算纹理坐标,而是使用完全相同的公式根据表示我们想要知道照明的方向的单位向量的三维坐标来计算纹理坐标。

环境贴图的最简单应用是为光线追踪器中未击中任何对象的光线赋予颜色:

1
2
3
4
5
6
7
8
trace_ray(ray, scene) {
if (surface = scene.intersect(ray)) {
return surface.shade(ray)
} else {
u, v = spheremap_coords(r.direction)
return texture_lookup(scene.env_map, u, v)
}
}

通过对光线追踪器进行这种更改,反射其他场景对象的光滑对象现在也将反射背景环境。

在栅格化上下文中,可以通过将镜面反射添加到着色计算中来实现类似的效果,其计算方式与光线追踪器相同,但只是直接在环境贴图中查找,而不考虑场景中的其他对象:

1
2
3
4
5
6
shade_fragment(view_dir, normal) {
out_color = diffuse_shading(k_d, normal)
out_color += specular_shading(k_s, view_dir, normal)
u, v = spheremap_coords(reflect(view_dir, normal))
out_color += k_m ⋆ texture_lookup(environment_map, u, v)
}

这种技术被称为反射映射。

环境贴图的更高级用法计算了来自环境贴图的全部照明,而不仅仅是镜面反射。这就是环境照明,在光线追踪器中可以使用蒙特卡罗积分计算,在栅格化中可以通过近似环境为一组点源并计算许多阴影映射来计算。

环境贴图可以存储在用于映射球体的任何坐标系中。球形(经度-纬度)坐标是一个流行的选项,尽管在极点处贴图的压缩会浪费纹理分辨率并且可能在极点处创建伪像。立方贴图是更有效的选择,在交互式应用程序中广泛使用(图 11.23)。

fig11_23.jpg

图 11.23。圣彼得大教堂的立方贴图,将六个面存储在展开的“水平交叉”排列中的一个图像中。(纹理:Emil Persson)

11.5 程序 3D 纹理

在之前的章节中,我们使用 crc_r 作为物体上某点的漫反射率。对于没有实色的物体,我们可以用一个函数 cr(p)c_r(p) 替换它,将 3D 点映射到 RGB 颜色(Peachey,1985 年;Perlin,1985 年)。这个函数可能只返回包含 pp 的物体的反射率。但是对于具有纹理的物体,我们应该期望 cr(p)cr(p) 随着 pp 在表面上移动而变化。

定义从 3D 表面到 2D 纹理域的纹理映射函数的替代方法是创建一个 3D 纹理,它在 3D 空间中的每个点定义了 RGB 值。我们仅会在表面上调用它,但通常比为任意表面上的潜在奇怪的 2D 子集点定义其要容易得多。关于 3D 纹理映射的好处是很容易定义映射函数,因为表面已经嵌入到 3D 空间中,并且从 3D 到纹理空间的映射没有扭曲。这种策略显然适用于从固体介质“雕刻”出来的表面,例如大理石雕塑。

3D 纹理的缺点是将它们存储为三维光栅图像或体积会消耗大量内存。因此,3D 纹理坐标通常与程序纹理一起使用,其中纹理值是使用数学过程计算而不是通过从纹理图像中查找它们来计算的。在本节中,我们将看一下用于定义程序纹理的一些基本工具。虽然这些也可以用于定义 2D 程序纹理,但在 2D 中更常见的是使用栅格纹理图像。

11.5.1 3D 条纹纹理

有很多方法可以制作条纹纹理。假设我们有两种颜色 c0c_0c1c_1,想要用它们来制作条纹纹理。我们需要一些能够交替使用这两种颜色的振荡函数。一个简单的方法是正弦函数:

88108472d5c70174b8248af7a908c298.png

我们还可以通过参数 ww 使条纹的宽度可控:

b5230c50444552e260bb478b6654eaf5.png

如果我们希望在两种条纹颜色之间平滑插值,则可以使用参数 t 线性变化颜色:

b8293784e5495146397c50666a5f059f.png

这三种可能性如图 11.24 所示。

fig11_24.jpg

图 11.24。从绘制一组保持 zz 不变的 xyxy 点而得到各种条纹纹理。

11.5.2 实体噪声

尽管常规纹理(如条纹)通常很有用,但我们希望能够制作出“斑驳”的纹理,例如我们在鸟蛋上看到的纹理。这通常是通过使用一种“实体噪声”来实现的,通常被称为 Perlin 噪声,以其发明者的名字命名,他因其在电影行业中的影响而获得了技术学院奖(Perlin,1985 年)。

对于每个点调用随机数以获得噪声外观并不合适,因为它就像电视静态中的“白噪声”。我们希望使其更平滑,同时保留随机质量。一种可能性是模糊化白噪声,但没有实际的实现方法。另一种可能性是创建一个大型格子,在每个格点上放置一个随机数,然后对格节点之间的新点进行随机插值;这只是上一节中描述的具有随机数的 3D 纹理阵列。这种技术会使格子过于明显。Perlin 使用了各种技巧来改进这种基本的格子技术,以便使格子不那么明显。这导致了一组相当华丽的步骤,但基本上只有从线性插值 3D 随机值数组中进行三个更改。第一个更改是使用 Hermite 插值来避免机器带,就像在常规纹理中可以做的那样。第二个变化是使用随机向量而不是值,并使用点积来推导随机数;这通过将局部最小值和最大值移动到网格顶点之外,使底层网格结构不那么明显。第三个变化是使用 1D 数组和散列来创建虚拟的 3D 随机向量阵列。这会增加计算以降低内存使用。这是他的基本方法:

这里给出 Perlin 噪声的基本方法。噪声值 n(x,y,z)n(x,y,z) 为:

0e0a2d71c18428ef32870736e03f3b80.png

其中 (x,y,z)(x,y,z)xx 的笛卡尔坐标,

Ωijk(u,v,w)=ω(u)ω(v)ω(w)(Γijk(u,v,w))\begin{array}{c} \Omega_{i j k}(u, v, w)=\omega(u) \omega(v) \omega(w)\left(\Gamma_{i j k} \cdot(u, v, w)\right) \end{array}

ω(t)ω(t) 为三次加权函数:

ω(t)={2t33t2+1 if t<10 otherwise \begin{array}{c} \omega(t)=\left\{\begin{array}{ll} 2|t|^{3}-3|t|^{2}+1 & \text { if }|t|<1 \\ 0 & \text { otherwise } \end{array}\right. \end{array}

最后一个关键步骤是,Γijk\Gamma_{ijk} 是格点 (x,y,z)=(i,j,k)(x,y,z) = (i,j,k) 的随机单位向量。由于我们希望任意潜在 ijkijk,因此我们使用伪随机表:

Γijk=G(ϕ(i+ϕ(j+ϕ(k)))),\begin{array}{c} \Gamma_{i j k}=\mathbf{G}(\phi(i+\phi(j+\phi(k)))), \end{array}

其中 GG 是预先计算好的 nn 个随机单位向量的数组,ϕ(i)=P[i mod n]ϕ(i)=P[i \space \bold{mod} \space n]PP 是长度为 nn 的数组,包含 00n1n-1 之间整数的一种排列。在实践中,Perlin 建议将 nn 设置为 256256

要选择随机单位向量 (vx,vy,vz)(v_x,v_y,v_z),首先设置

vx=2ξ1,vy=2ξ1,vz=2ξ1,\begin{array}{l} v_{x}=2 \xi-1, \\ v_{y}=2 \xi^{\prime}-1, \\ v_{z}=2 \xi^{\prime \prime}-1, \end{array}

其中 ξ,ξ,ξ\xi, \xi', \xi'' 是规范化的随机数(在区间 [0,1)[0,1) 内均匀分布)。然后,如果 (υx2+υy2+υz2)<1(υ_x^2+υ_y^2+υ_z^2)<1,则将向量设置为单位向量。否则,继续随机设置,直到长度小于 1,然后将其设为单位向量。这是一个拒绝方法的例子,在第 13 章中将更详细地讨论。基本上,“小于”测试得到了单位球中的随机点,从原点到该点的向量是均匀随机的。这在立方体中的随机点上不成立,因此我们通过测试“除去”角落。

由于实体噪声可以是正数或负数,因此必须在转换为颜色之前进行转换。图 11.25 显示了 10×1010×10 平面上噪声的绝对值和拉伸版本。这些版本通过缩放输入到噪声函数的点来进行拉伸。

fig11_25.jpg

图 11.25。实体噪声的绝对值以及缩放后的 xxyy 值的噪声。

暗曲线是原始噪声函数从正数变为负数的地方。由于噪声变化范围是 1-111,因此可以通过使用 (noise+1)/2(noise + 1)/2 来获取颜色的平滑图像。然而,由于接近 111-1 的噪声值很少,因此这将是一个相当平滑的图像。较大的缩放可以增加对比度(图 11.26)。

fig11_26.jpg

图 11.26。使用 0.5(noise+1)0.5(noise+1)(a)和 0.8(noise+1)0.8(noise+1)(b)作为强度。

11.5.3 紊流

许多自然纹理在同一纹理中包含各种特征尺寸。Perlin 使用伪分形“紊流”函数:

nt(x)=in(2ix)2i\begin{array}{l} n_{t}(\mathbf{x})=\sum_{i} \frac{\left|n\left(2^{i} \mathbf{x}\right)\right|}{2^{i}} \end{array}

这有效地重复将噪声函数的缩放副本叠加在其上,如图 11.27 所示。

fig11_27.jpg

图 11.27。具有(从左上到右下)一到八个求和项的紊流函数。

可以使用紊流来扭曲条纹函数:

84b7a21ec86f1d00ed96f79a1f542c73.png

使用不同的 k1 和 k2 值生成了图 11.28。

fig11_28.jpg

常见问题

如何在光线追踪中实现置换映射?

没有理想的方法。生成所有三角形并在必要时缓存几何体将防止内存超载(Pharr&Hanrahan,1996; Pharr,Kolb,Gershbein 和 Hanrahan,1997)。当位移函数受限时,尝试直接交错位移表面是可能的(Patterson,Hoggar&Logie,1991; Heidrich&Seidel,1998; Smits,Shirley 和 Stark,2000)。

为什么我的带纹理的图像看起来不真实?

人类擅长看到表面上的小瑕疵。计算机生成的使用纹理贴图进行细节的图像通常缺乏几何缺陷,因此看起来“过于平滑”。

注释

透视校正纹理的讨论基于 Fast Shadows and Lighting Effects Using Texture Mapping(Segal,Korobkin,van Widenfelt,Foran 和 Haeberli,1992)以及 3D Game Engine Design(Eberly,2000)。

练习

  1. 使用表面和实体技术找到几种实现无限 2D 棋盘格的方法。哪种方法最好?
  2. 使用暴力代数验证等式(9.4)是有效的相等关系。
  3. 如何使用 zz 缓冲深度和矩阵变换实现实体纹理?
  4. mipmap_sample_trilinear 函数扩展为单个函数。