首发于 虎书第4版
Fundamentals Of Computer Graphics 第十一章 纹理映射(上)

Fundamentals Of Computer Graphics 第十一章 纹理映射(上)

本文翻译虎书第十一章纹理映射的11.1~11.2部分

纹理映射

在尝试复制真实世界的外貌时,人们很快意识到几乎任何物质的表面都是有特征的。木材长有纹理。皮肤长有皱纹;布料会有编织结构;油画会有画刷或滚筒留下的痕迹,即使是光滑的塑料也会有制作他的模具的凹凸不平的痕迹,并且光滑金属显示出机器加工过程的痕迹。材料的没有特征的地方很快也会被斑点,凹痕,污点,划痕,指纹和污垢覆盖。

在图形学中,我们把这些现象统称为“空间上表面属性的变化”——表面的属性在每一处地方都在发生着变化,但并没有真正意义上改变表面的 形状 ,考虑到这些因素,所有的建模和渲染系统都提供了一些 纹理映射 的涵义:使用一个被称作 纹理贴图 纹理图像 ,或者仅仅是一个 纹理 ,来存储这些你想要在一个表面上展示的细节,然后进行数学处理,把这个图片“映射”到表面上。

事实证明,一旦存在将图像映射到表面的机制,就会有很多奇淫技巧,这些技巧已经超过了我们介绍表面细节的基本目标。纹理可以用来制作阴影和反射,提供光照信息,甚至定义表面形状。在复杂的交互程序中,纹理经常被用于存储与其作为一张图片毫无干系的的数据( 这里不知道怎么组织语言了,举个例子就是我们可以把骨骼动画烘焙到一张纹理中,从而让GPU去执行蒙皮操作(GPU Skining),提高性能 )。

本章节讨论使用纹理表示表面细节,阴影和反射。尽管这些基础思想都很简单,但存在一些问题会复杂化纹理的使用。首先,纹理容易失真,并且设计将纹理映射到表面的功能是有挑战性的。另外,纹理映射是一个重新采样的过程,就像重新缩放一张图片一样,正如我们在第九章所说,重采样很容易引入锯齿失真问题。使用纹理贴图和动画的组合很容易产生明显的混叠效应,很多纹理映射系统很复杂就是因为其中包含为了修复这些失真的 抗锯齿 方案。

11.1 查询纹理值

首先,让我们考虑一个简单的纹理映射应用,我们有一个有木地板的场景,我们希望地板的漫反射颜色由一个显示着木板木纹的图片控制。不管我们使用光线追踪还是光栅化,用于计算射线表面交点或一个由光栅化器生成的片段的颜色的着色代码都需要知道在光照点的纹理颜色,以便把它作为我们在第十章提到的Lambertian着色模型中的漫反射颜色。

为了得到这个颜色,这个着色器执行 纹理查询(texture lookup) :它计算出在纹理图片的本地坐标系中对应着色的点,并且读出在这个纹理图片在这个点的颜色,也就是 纹理样品(texture sample) ,这个颜色被用在着色中,并且,由于对于每个在地板上可见的像素都在这个纹理不同的位置上发生了纹理查询,图像中会出现不同的颜色。这代码看起来可能像这样:

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
}

在这段代码中,着色器查询表面在纹理中的位置,对于我们想要进行着色的每个表面,这个纹理都要能够回应这个查询。这告诉了我们纹理映射的第一个关键要素:我们需要一个函数,他可以从表面映射到纹理,这样我们可以轻松地为每个像素进行计算。这就是 纹理坐标函数 (图 11.1)

图11.1 就像观察投影映射π每个在对象上的表面S到图片上,纹理坐标函数映射Φ每个在对象上的表面S到纹理上的T,恰当的定义函数Φ是所有纹理映射应用的基础

并且我们说它为每个表面上的点分配纹理坐标。从数学的角度讲,他是一个从表面 S 到纹理域 T 的映射

\phi : S → T

: (x, y, z) \mapsto (u, v).

集合 T 通常被称作“纹理空间”,通常只是一个矩形,其中包含这个图片;通常使用单位化的正方形 (u,v)\in\left[0,1\right]^2 (在本书中,我们将会使用 u,v 来表示两个 纹理坐标 ),在许多方面他与第七章的讨论的观察投影很相似,例如在本章中被称为π的观察投影,用于映射场景中表面上的点到图片中的点;两者都是从3D到2D的映射,还都是需要渲染的——一个需要知道从哪里获取纹理的值,一个需要知道把着色结果放在图片中的哪个位置,但是他们也有一些不同点:π几乎总是一个透视或正交投影,而 \phi 可以采用多种方式;并且对于一张图片只有一个观察投影,而场景中的每个对象都可能具有完全独立的纹理坐标函数。

当我们的目标是把纹理贴到表面上时, \phi 作为从表面到纹理的映射可能会让人稍感惊讶,但这正是我们所需要的函数。

对于木地板的情况,如果地板只位于常数 z 并且平行于 x y 轴的位置点,我们可以使用如下映射

u = ax; v = by,

恰当选择一些缩放因子 a b ,指定对于 (x,y,z)_{floor} 点的纹理坐标 (s,t) ,然后使用纹理像素的值,或最接近 (u,v) 的在纹理上 (x,y) 点的 texel 值。我们可以用这种方式渲染出如图11.2的图片。

图 11.2 一个木地板,直接使用了一个简单使用了x和y的坐标点的纹理坐标函数

不过这是一个相当局限的例子:如果以相对于 x y 轴一定角度对房间进行建模,或者我们想要把这个木材纹理使用在椅子的弧形靠背上呢?我们将会需要一个更好的方式来为表面上的点计算纹理坐标。

最简单的纹理映射方式引起的另一个问题可以通过从一个非常离谱的掠角来渲染一张高对比度的纹理到一张低分辨率的图片上被显著表现出来。图11.3展示了一个大平面使用同样的方法进行贴图,但是它使用了一个高对比度的网格图案,并且从一个几乎水平的角度来观察。

图11.3 一个大的平面,使用同图11.2同样的方法进行贴图,表现出严重的锯齿失真现象

你可以看到有锯齿失真(前面的还仅仅是有些阶梯状锯齿失真,远处就是波浪和闪闪发光的图案了),它类似于第九章提到的在图片重采样过程未使用恰当的滤波器而出现的现象。虽然我们需要一种极端情况来让这个失真明显的体现在本书上的一张小图上,在动画中,这些图案还会四处移动,并且当他们更加微妙时,会非常分散注意力。

我们现在看到了这种基础纹理映射方式两个主要问题:

  • 定义纹理坐标函数
  • 查询纹理值而不会引起过多的混叠

这两个顾虑是所有纹理映射程序的基础并且会被在11.2和11.3节讨论。一旦你理解了他们和对应的解决方案,你就理解了纹理映射。剩余的是如何将基础的纹理处理工具应用于各种不同的目的,这将会在11.4节进行讨论。

11.2 纹理坐标函数

设计好一个纹理坐标函数 \phi 是获取好的纹理映射结果的关键所在。你可以认为它将决定如何变形一个平面或者说矩形图片以便与你想要绘制的3D表面相符合。或者,你正在将表面变得平整,而不是使其起褶皱,撕裂或折叠,以便他平整的贴在图片上。有些时候他是简单的:有时这个3D模型已经是一个平面矩形了!在其他情况下它将会非常棘手:3D形状可能非常复杂,就像一个角色的身体。

定义纹理坐标函数的问题对于计算机图形学已经不是一个新问题了。制图师在设计覆盖地球表面大面积地图时面临着完全相同的问题:从弯曲的地球到平面地图的映射不可避免的会导致地区,角度,距离的变形,这很容易导致地图产生误导。在数个世纪中提出的多个地图投影方案,都在平衡同样的竞争关系——最小化在一个连续区域中覆盖大面积区域导致的各种变形问题——这也正是纹理映射所面临的问题。

在一些应用中(就像我们将要在此章节的后面看到的)有一个明显的原因使用特定的映射。但是在更多的情况下,设计纹理坐标映射并平衡竞争关系是一个艰巨的任务,以至于娴熟的建模人员会投入大量的精力。

你可以通过任何你能想到的方式来定义 \phi 。但这里有一些值得考虑的先决条件:

  • 双射性 :在大部分情况下你会想 \phi 是双射性的(参见2.1.1节),所以表面上每个点都会映射到纹理空间的不同点上。如果一些点映射到同一的纹理空间点,那么一个在纹理上的点将会影响多个在表面上的点。如果你现在表面重复一个纹理(想象一下墙纸或地毯),故意进行多对一的从表面到纹理的映射是应该的,但是有时你不会想这样。
  • 尺寸变形 :纹理的比例应大致恒定在表面的大小。也就是说,表面上任何靠近在一起的点相对于纹理中同样的点都有着同样的距离。就函数 \phi 而言, \phi 的导数不应有太大的变化。
  • 形状变形 :纹理不应当变形的很严重。也就是说,一个绘制在表面上的小圆应当映射到一个纹理上的合理的圆形上,而不是一个被极端挤压或拉长的形状。就 \phi 而言, \phi 的导数不应该在不同的方向有太大的不同。
  • 连续性 :不应当有太多的接缝:表面上的相邻点应当映射到纹理上的相邻点。也就是说, \phi 应该是连续的,或者尽可能少的不连续。在很多情况下,一些不连续的情况是不可避免的,我们希望把他们放在不显眼的位置。

通过参数方程式定义的表面(参见2.5.8节)带有内置的纹理坐标函数选项:只需要简单的将定义表面的函数反转,然后使用两个表面的参数作为纹理坐标。这些纹理坐标可能具有,也可能不具有理想的属性,这取决于表面,但是他们确实提供了映射。

但是对于那些隐式定义的,或者仅仅通过一个三角网格定义的表面,我们需要不依赖一个现有的参数的情况下用一些其他的方式来定义纹理坐标。宽泛的说,这两种定义纹理坐标的方式是根据表面点的空间坐标或者对于网格表面,存储他们在顶点的纹理坐标并且在表面上进行插值从而几何的计算他们。让我们一次性看个够这些选项。

11.2.1 几何确定的坐标

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

我们将通过映射测试图11.4的图片到表面上来说明各种纹理坐标函数。

图11.4 测试图片

图片中的数字让你可以读到大概的坐标($u,v$),并且格子让你看到这个映射是如何变形的。

平面投影

从3D到2D的映射中可能最简单的映射是平行投影——与正交视图中使用的映射一样(如图11.5)。

图11.5 如果投影的方向被选择为大致沿着总体法线方向,平面投影为起始处几乎是平坦的对象或对象的一部分提供了一个有用的参数

我们设计的这个机制之前已经看过了(参见7.1节),可以被直接用于定义纹理坐标:就像正交视图归结于通过乘以一个矩阵并丢弃 z 分量,通过平面投影生成纹理坐标可以通过一个简单的矩阵乘法实现:

其中纹理矩阵 M_t 表示一个仿射变换,而 * 表示我们不关心第三个坐标。

他在几乎是平面,并且在平面的法线没有太多变化的表面上工作的很好,并且通过取法线的平均值可以得到一个好的投影方向。对于任何闭合的形状,然而一个平面投影并不具备内射性:在前面和后面的点都会映射到纹理空间中的相同的点(图 11.6)

图 11.6 在一个封闭对象上使用一个平面投影总会产生一个非内射的结果,一对多的映射,并且当投影方向与表面相切时其附近的点将会极端变形

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

图 11.7 一个使用了类似观察变换的透视纹理变换会投影到一个点上

现在这个4X4的矩阵 P_t 表现了一个投影(不一定是仿射)变换——也就是说最后一行可能不是 [0,0,0,1]

投影纹理坐标在阴影贴图技术中是很重要的,将会在11.4.4节讨论。

球坐标

对于球体,维度/经度参数化是众所周知并且已经被广泛使用。极点附近有很多可以导致困难的变形,但是仅沿着一条纬线可以不连续的覆盖整个球体。

大致的球形表面可以使用一个映射表面上的点到球上的点的(通过径向投影(从球体中心到平面上的点画一条线,并找到与球的交点))纹理坐标函数来被参数化。此交点的球坐标是在这个表面上起始点的纹理坐标。

另一种说法是,你可以使用球坐标 (\rho,θ,\phi) 来表示表面的点,然后丢弃 \rho 坐标并将θ和 \phi 分别映射到[0,1]范围内。该公式依赖于球坐标的特性;在2.5.8节有提及。

\phi(x,y,z)\;=(\frac{\;\lbrack\pi\;+\;\tan^{-2}(y,x)\rbrack}{2\pi},\frac{\lbrack\pi\;-\;{\displaystyle\frac{\cos^{-1}\left({\displaystyle\frac z{\left|\left|x\right|\right|}}\right)}\pi}\rbrack}\pi)

一个球坐标映射在所有地方都是双射的,除非在极点处(从其出发整个表面都是可见的)。极点附近继承了就像经纬映射一样的变形。图11.8 展示了一个球坐标提供了一个合适的纹理坐标函数的对象。

图11.8 对于这个比较圆润的cube,将每个点投影到以这个物体中心为中心的球上提供了一个内射的映射,他使用了曾经被用于地球上的纹理贴图。注意一些远离中心的区域被放大了(表面的点在纹理空间中拥挤在一起),靠近中心的点被缩小了。

圆柱坐标

对于柱状而非球状的物体,从一个轴到圆柱的投影可能比从一个点到球的投影工作的更好(如图 11.9所示)。



类似于球形投影,这相当于转换为圆柱坐标并舍弃半径:

\phi(x,y,z)\;=(\frac{\;\lbrack\pi\;+\;\tan^{-2}(y,x)\rbrack}{2\pi},\frac{\lbrack1+z\rbrack}2)

立方体贴图

使用球坐标来参数化球或类似球形的图像导致了在几点附近的形状和区域的高度变形,这通常导致肉眼可见的失真,会有两个特殊的点显示着纹理异常。一个流行的替代方案更加整齐,但代价是更多的不连续性。这个想法是投影到一个立方体上,而不是一个球体,然后对立方体的六个面使用六个单独的正方形纹理。这个六个正方形纹理集合被称为 立方体贴图 。这会在每个立方体边上引起中断,但是他保持着形状和区域的低失真。

计算立方体贴图纹理坐标同样比球形的代价小,因为投影到一个平面上仅仅需要一个除法——本质上和观察透视投影相同。举个例子,对于一个投影在立方体 +z 面的点:

(x,y,z)\;\mapsto(\frac xz,\frac yz)

立方体贴图的一个令人困惑的方面是如何建立被定义在六个面上 u v 的方向的约定。任何约定都可以,但是这个约定会影响到纹理的内容,所以标准化是重要的。

因为立方体纹理经常被用在在立方体内部观察的纹理(参见11.4.5节中环境映射),这个常见的约定包含定向的 u v 轴所以从内部看 u 是相对于 v 的顺时针方向的。这个在OpenGL中被使用的约定为:

\phi_{-x}(x,y,z)\;=\;\frac{\lbrack1\;+\;{\displaystyle\frac{(+z,\;-y)}{\left|x\right|}}\rbrack}2,\\phi_{+x}(x,y,z)\;=\;\frac{\lbrack1\;+\;{\displaystyle\frac{(-z,\;-y)}{\left|x\right|}}\rbrack}2,\\phi_{-y}(x,y,z)\;=\;\frac{\lbrack1\;+\;{\displaystyle\frac{(+x,\;-z)}{\left|y\right|}}\rbrack}2,\\phi_{+y}(x,y,z)\;=\;\frac{\lbrack1\;+\;{\displaystyle\frac{(+x,\;+z)}{\left|y\right|}}\rbrack}2,\\phi_{-z}(x,y,z)\;=\;\frac{\lbrack1\;+\;{\displaystyle\frac{(-x,\;-y)}{\left|z\right|}}\rbrack}2,\\phi_{+z}(x,y,z)\;=\;\frac{\lbrack1\;+\;{\displaystyle\frac{(+x,\;-y)}{\left|z\right|}}\rbrack}2.

下标指示每个投影对应于立方体的哪一面,例如 \phi_{-x} 用于在 x = +1 时投影到立方体表面的某点。你可以通过查看坐标最大绝对值来得知一个点被投影到哪个面上,例如:如果 |x| > |y| ,并且 |x| > |z| ,这个点投影到 +x -x 面上,取决于 x 的符号。

一个被用于立方体贴图的纹理有六片正方形。(参见图11.10)。

图11.10 一个被投影到一个立方体贴图的表面。表面上的点从中间往外部投影,每个点都会被投影到六个面中的其中一个。

通常他们被打包在一起放在一个图片中存储,像一个立方体被展开似的进行排列。

11.2.2 纹理坐标的插值

要对一个三角网格表面的纹理坐标进行更加精细的控制,你可以显式存储每个顶点处的纹理坐标,并且通过重心插值的方式把它们插入到三角形中(参见8.1.2节)。它的工作方式和你可能在网格上定义的其他任何平滑变化的变量完全相同,例如颜色,法线甚至是3D位置本身。

让我们来看一个单个三角形的例子。图11.11展示了一个被我们熟悉的测试图案的一部分映射的三角形纹理。

图11.11 一个使用了线性插值的纹理坐标的三角形。左图:在纹理空间下被绘制的三角形;右图:3D空间下被绘制的三角形。

通过观察三角形上的图案,我们可以推断出三个顶点的纹理坐标是(0.2,0.2),(0.8,0.2),(0.2,0.8),因为这些是纹理中出现在三角形三个角上的点。就像上一节中的几何确定的映射一样,我们通过提供从表面到纹理域的映射来控制哪里的纹理会贴在表面上,在这种情况下,通过指定每个顶点对应的纹理空间中的位置。定位顶点后,通过三角形的线性(重心)插值负责剩余部分。

在图11.12中,我们在整个网格上展示了一个可视化纹理的常用方法:简单的在纹理空间使用那些在他们纹理坐标中定位的顶点来绘制三角形。

图11.12 一个在纹理空间中坐落它的三角形的二十面体,来达成0失真但是有很多接缝的效果。

这个可视化展示给我们纹理的哪些部分被哪些三角形使用,他是评估纹理坐标的便捷工具,并且可以用来调试各种纹理映射的代码。

纹理坐标映射的质量被取决于顶点纹理坐标——也就是说,取决于网格是如何坐落在纹理空间中的。无论被指定了什么坐标,只要网格中的三角形共享顶点(参见12.1节),它的纹理坐标映射总是连续的,因为相邻的三角形将会在其共享边上的纹理坐标达成一致。但是上面描述的其他不理想的品质将不会这么自动化。内射性意味着三角形不会再纹理空间中重叠,如果他们重叠了,意味着有一些在纹理中的点会出现在表面的多个位置。

当在纹理空间中的三角形区域按比例对应他们在3D空间中的区域时,尺寸失真会比较低。举个例子,如果角色的脸被一个连续的纹理坐标函数映射,往往会因为鼻子被挤压到纹理空间中一个相对比较小的地方而终止,就像图11.13展示的那样。

图11.13 一个脸部模型,使用了纹理映射来获取合理的低形状失真,但是仍然展现出中等程度的失真。

尽管在鼻子上的三角形小于脸颊上的三角形,大小的比例在纹理空间中更加极端。结果是鼻子上的纹理变大了,因为小区域的纹理覆盖大区域的表面。类似的,对比额头和太阳穴,他们三角形在3D空间大小差不多,但是太阳穴附近的三角形在纹理空间中更大些,这就导致了纹理表现得更小些。

类似的,在三角形形状在3D空间和纹理空间类似时,形状失真也较低。这个脸的例子形状失真比较小,但是,举个例子,在图11.17中的球体在极点附近有非常大的形状失真。

11.2.3 平铺,拼接模式,和纹理转换

允许纹理坐标超出纹理图片的边界通常很有用。有时有这样一个细节:纹理坐标中计算舍入误差可能会让一个本该精确落在纹理边界上的顶点落在稍外面一点,在这种情况下,纹理映射机制不应当失败,但是它还可能是一个建模工具。

如果一个纹理仅支持覆盖表面的一部分,但是纹理坐标已经设置为映射整个表面到单位方块上了,一个选项是准备一个纹理图片,这个图片大部分是空白的,只在一块小地方有内容。但是这将会需要一个非常高分辨率纹理图片来获取相关区域足够的细节。另一个选择是缩放所有的纹理坐标来让他们覆盖更大的范围——[-4.5,5.5]x[-4.5,5.5]作为例子,位于表面中心的单位正方形会变成十分之一大小。

对于这种情况,纹理查询在被纹理图片覆盖的单位正方形之外的区域将会返回一个恒定的背景颜色。一种方式是设置一个背景颜色,他将会在查询单位正方形外区域时返回。如果纹理图片已经有一个恒定的背景颜色(举个例子,一个白色背景的Logo),另一种方式是自动拓展背景颜色,他会返回最靠近边缘的纹理点的颜色,通过 拉伸(Clamping) 图片中第一个像素到最后一个像素的 u v 坐标来获取。

有时我们想要一个重复的图案,比如一个棋盘格,瓷砖地板或者一个砖墙。如果图案在一个矩形格内重复,类似于创建一个使用了一些相同数据复制的图片是很浪费的。相反的,纹理查询超过纹理图片时,我们可以使用平铺的索引来处理:当查询点超过纹理图片的右边界时,他会使用左边界平铺。使用整数取余运算可以非常简单的处理像素的坐标:

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.14 通过拼接纹理像素坐标将一个木地板纹理平铺在纹理空间

当我们使用一个图片指定了一个纹理,这些模式描述了如何使用有限的图片数据来定义这个方法。在11.5节中,我们将会看到程序化有限图片数据会自然地在无限的平面上拓展,因为他们不受有限的图像数据限制。由于两者在逻辑上是无限的,两种类型的纹理可以互换。

当调整纹理的缩放和位置时,避免真正修改生成纹理坐标的方法是有益的,或者将纹理坐标存储在网格顶点中,而不是在对纹理进行采样前应用矩阵变换:

\phi(x)\;=\;M_T\phi_{model}(x)

\phi_{model} 是模型提供的纹理坐标函数, M_T 是一个3x3的矩阵表示一个使用齐次坐标进行2D纹理坐标的仿射或投影变换。这样的一个变换,有时被限制为只能进行缩放或平移,被大部分使用纹理映射的渲染器支持。

11.2.4 正确的透视插值

对三角形进行纹理坐标插值来获得正确的透视视觉效果是有些不妥之处的,但是我们可以在光栅化阶段解决这个问题。事情不简单的原因是只对在屏幕空间对纹理进行插值导致错误的图片,就像图11.15展示的网格纹理一样。

图11.15 左:正确的透视,右:在屏幕空间的插值

因为在透视视图中的物品会有远大近小的特性,在3D中均匀间隔的线要压缩到2D空间,需要更加仔细地对纹理坐标进行插值来完成这个目标。

我们可以通过对 (u,v) 坐标进行插值来实现三角形上的纹理映射,按8.1.2节修改光栅化的方法,但是这会导致图11.15右图所示的问题。如果屏幕空间的重心坐标被使用在了下面的光栅化代码中,那么类似的的问题会在三角形上出现。

for all x do
    for all y do
        compute (α, β, γ) for (x, y)
        if α  (0, 1) and β  (0, 1) and γ  (0, 1) then
            t = αt0 + βt1 + γt2
            drawpixel (x, y) with color texture(t) for a solid texture
            or with texture(β, γ) for a 2D texture

这个代码会生成有问题的图片。为了解决问题,让我们思考从世界空间$q$到齐次点$r$再到齐次化的点 s 的步骤:

纹理坐标插值问题最简单的形式是当我们有两个纹理坐标 (u,v)q Q 时,我们需要在图片中的以 s S 为端点的线上生成他们的纹理坐标,如果世界空间以 q Q 为端点的线上存在一点 q' 他会投影到屏幕空间的以 s S 为端点的线上点 s' ,这两个点会有同样的纹理坐标。

上面那个朴素的屏幕空间的算法表明:对于点 s' = s + α(S - s) 我们会使用纹理坐标 u_s + α(u_S - u_s) v_s + α(v_S - v_s) 表示。这会因为被映射到 s' 的世界空间的点 q' 不满足公式 q' = q + α(Q - q) 而导致公式结果不正确。

但我们从7.4节可以得知在线段 qQ 上的点一定会落到线段 sS 上某个位置上,事实上:

q + t(Q - q) \mapsto s + α(S + s)

这个插值参数 t α 是不一样的,但我们可以计算他们中的其中一个从而得到另一个:

t(\alpha)\;=\;\frac{w_r\alpha}{w_R\;+\;\alpha(w_r\;-\;w_R)}\;and\;\alpha(t)\;=\;\frac{w_Rt}{w_r\;+\;t(w_R\;-\;w_r)}

这些方程式为解决屏幕空间的插值提供了一种可能的修复方案。可以通过计算 u_s'\;=\;u_s\;+\;t(\alpha)(u_S\;-\;u_s) v_s'\;=\;v_s\;+\;t(\alpha)(v_S\;-\;v_s) 得到屏幕空间中点 s' = s + α(S - s) 的纹理坐标。这些正是映射到点 s' 的点 q' 的坐标,所以他仍然有效。但是对于每个片段都计算是很慢的,并且这里有一个更加简便的方案。

关键的观察结果是,因为我们知道透视变换会保留线和面,在三角形上线性插入任何我们想要的属性都是安全的,但是前提是他们必须与点一起经过透视变换。要得到几何理论,请减少尺寸,以便我们有齐次点 (x_r,y_r,w_r) 以及一个被插入的属性 u ,属性 u 应该是 x_r和y_r 的线性函数,因此,如果将 u 绘制为 (x_r,y_r) 上的高度场,结果会是一个平面。现在我们将 u 视为第三个空间坐标(将他称之为 u_r 来强调其与其他坐标被同等看待),然后通过透视变换发送整个3D齐次点 (x_r,y_r,z_r,w_r) ,结果 (x_s,y_s,z_s) 仍然会生成坐落在平面上的点。平面内部会有一些弯曲,但保持平坦。这意味着 u_s 是一个 (x_s,y_s) 的线性函数,也就是说,我们可以使用基于 (x_s,y_s) 坐标的线性插值在任何地方计算 u_s

图11.16 屏幕空间插值的几何推理。 上:$u_r$被插值为$(x_r,y_r)$的线性函数。 下:从$(x_r,y_r,u_r,w_r)$到$(x_s,y_s,u_s,1)$的透视变换后,$u_s$是$(x_s,y_s)$的线性函数

回到完整的问题,我们需要对作为世界空间坐标 (x_q,y_q,z_q) 线性函数的纹理坐标 (u,v) 进行插值。将这些点转换到屏幕空间,并添加纹理坐标(就像它们是其他坐标一样)之后,

上一段的实际含义是,我们可以继续对所有基于 (x_s,y_s) 的值(包括z缓冲区中使用的 z_s 值)进行插值,朴素方法的问题很简单,就是因为我们我们为插值选择了不一致的分量——只要涉及的变量是从透视之前或之后划分的,所有的数据都不会有问题。

剩下的一个问题是 (\frac u{w_r},\frac v{w_r}) 对于查找纹理数据不是直接有用的。我们需要的是 (u,v) 。这解释了我们放入的附加参数的目的(参见11.2),该参数的值始终为1:一旦有了 \frac u{w_r} \frac v{w_r} \frac 1{w_r} ,我们就可以轻松地通过除法来得到 (u,v)

为了确定这些推论是正确的,让我们检查再屏幕空间中对 \frac 1{w_r} 的插值确实会产生在世界空间中对 w_r 插值的倒数。

\frac1{w_r}+\alpha(t)(\frac1{w_R}\;-\;\frac1{w_r})\;=\;\frac1{w_r'}\;=\;\frac1{w_r+t(w_R\;-\;w_r)}

这种在转换后的空间中无误差地对 \frac 1{w_r} 线性插值的能力使我们能够正确构造三角形。我们可以利用这些定理来修改我们的扫描转换代码,获取已经通过观察矩阵但尚未经过齐次化的三个点 t_i = (x_i,y_i,z_i,w_i) ,并带有纹理坐标 t_i = (u_i,v_i)

for all xs do
    for all ys do
        compute (α, β, γ) for (xs, ys)
        if (α  [0, 1] and β  [0, 1] and γ  [0, 1]) then