《Fundamentals of Computer Graphics》5th(计算机图形学基础/虎书),中文翻译。
第 8 章 Viewing 视图
在前一章中,我们看到了如何使用矩阵变换来排列二维或三维空间中的几何对象。几何变换的第二个重要用途是将对象从它们在三维世界中的位置移动到它们在三维世界的二维视图中的位置。这种从三维到二维的映射称为视图变换 (viewing transformation),它在物体顺序渲染中起着重要作用,因为我们需要快速找到场景中每个对象在图像空间中的位置。
在第四章中学习光线追踪时,我们涵盖了不同类型的透视和正交视图以及如何根据任何给定的视图生成观察光线。而本章则是关于该过程的逆向处理。在这里,我们解释如何使用矩阵变换来表达任何平行或透视视图。本章中的变换将把场景中的三维点(世界空间)投影到图像(图像空间)中的二维点,并将给定像素的视线上的任何点投影回该像素在图像空间中的位置。
如果您最近没有查看过第四章中有关透视和光线生成的讨论,则建议您在阅读本章之前进行复习。
单独地,从世界到图像投影点的能力仅能用于产生线框 (wireframe) 渲染 —— 即只绘制对象的边缘,且更近的表面不会遮挡更远的表面(图 8.1)。就像光线追踪器需要沿着每条视线找到最接近的表面交点一样,显示实心对象的物体顺序渲染器必须计算出在屏幕上任何给定点处绘制的(可能多个)表面中哪一个最接近,并仅显示该表面。在本章中,我们假设正在绘制一个由 ( x , y , z ) (x,y,z) ( x , y , z ) 坐标指定其两个端点的三维线段模型。后面的章节将讨论需要产生实体表面渲染所需的机制。
图 8.1. (a) 正交投影下的线框立方体。 (b) 透视投影下的线框立方体。 (c) 具有隐藏线消除的透视投影。
观察变换的任务是将在规范坐标系中表示为 ( x , y , z ) (x,y,z) ( x , y , z ) 坐标的三维位置映射到以像素为单位表达的图像坐标中。它是一个复杂的东西,取决于许多不同的因素,包括相机位置和方向、投影类型、视场和图像分辨率。与所有复杂的变换一样,最好通过将其分解为几个较简单的变换的乘积来处理。大多数图形系统通过使用以下三个变换序列来实现:
相机变换或眼睛变换 (camera transformation or eye transformation),这是一个刚体变换,将相机放置在方便的方向上的原点。它仅取决于相机位置和方向或姿态。
投影变换 (projection transformation),将点从相机空间投影,以使所有可见点在 x 和 y 方向上都处于 -1 到 1 的范围内。它仅取决于所需的投影类型。
视口变换或窗口变换 (viewport transformation or windowing transformation),将此单位图像矩形映射到所需的像素坐标矩形。它仅取决于输出图像的大小和位置。
有些 API 仅将“观察变换”用于我们称之为相机变换的部分。
为了方便描述过程的各个阶段(图 8.2),我们给这些变换的输入和输出坐标系命名。
相机变换将规范坐标中的点(或者世界空间中的点)转换为相机坐标,或将它们放置在相机空间中。投影变换将点从相机空间移动到规范视图体积中。最后,视口变换将规范视图体积映射到屏幕空间。
每个变换都非常简单。我们将从视口变换开始详细讨论正交情况下的这些变换,然后介绍如何更改以支持透视投影。
其他名称:
相机空间也称为“眼睛空间”,相机变换有时也称为“观察变换”;
规范视图体积也称为“裁剪空间”或“归一化设备坐标”;
屏幕空间也称为“像素坐标”。
我们从一个问题开始,其解决方案将在任何观察条件下重复使用。我们假设要查看的几何体在规范视图体积中,并且希望使用指向-z 方向的正交相机进行查看。规范视图体积是包含所有三维点的立方体,其笛卡尔坐标在 -1 到 +1 之间 —— 即 ( x , y , z ) ∈ [ − 1 , 1 ] 3 (x,y,z)∈[-1,1]3 ( x , y , z ) ∈ [ − 1 , 1 ] 3 (图 8.3)。我们将 x = − 1 x = -1 x = − 1 投影到屏幕左侧,x = + 1 x = +1 x = + 1 投影到屏幕右侧,y = − 1 y = -1 y = − 1 投影到屏幕底部,y = + 1 y = +1 y = + 1 投影到屏幕顶部。
图 8.3。规范视体 (canonical view volume) 是一个以原点为中心,边长为 2 的立方体。
“规范”的词再次出现 —— 它表示为方便而任意选择的东西。例如,单位圆可以称为“规范圆”。
回想一下第 3 章中有关像素坐标的约定:每个像素“拥有”以整数坐标为中心的单位方形;图像边界从像素中心超出半个单位;最小像素中心坐标为 ( 0 , 0 ) (0,0) ( 0 , 0 ) 。如果我们将图形绘制到 n x n_x n x 乘 n y n_y n y 像素的图像或窗口中,则需要将正方形 [ − 1 , 1 ] 2 [-1,1]^2 [ − 1 , 1 ] 2 映射到矩形 [ − 0.5 , n x − 0.5 ] × [ − 0.5 , n y − 0.5 ] [-0.5,n_x-0.5]× [-0.5,n_y-0.5] [ − 0.5 , n x − 0.5 ] × [ − 0.5 , n y − 0.5 ] 。
将正方形映射到可能不同比例的矩形并不是问题。从规范坐标到像素坐标时,x x x 和 y y y 只需要使用不同的比例因子即可。
现在,我们将假设所有要绘制的线段都完全位于规范视图体积内。稍后,在讨论裁剪时,我们将放松这个假设。
由于视口变换将一个轴对齐的矩形映射到另一个轴对齐的矩形,因此它是由方程(7.6)给出的窗口变换的一种情况:
[ x screen y screen 1 ] = [ n x 2 0 n x − 1 2 0 n y 2 n y − 1 2 0 0 1 ] [ x canonical y canonical 1 ] . (8.1) \left[\begin{array}{c}
x_{\text {screen }} \\
y_{\text {screen }} \\
1
\end{array}\right]=\left[\begin{array}{ccc}
\frac{n_{x}}{2} & 0 & \frac{n_{x}-1}{2} \\
0 & \frac{n_{y}}{2} & \frac{n_{y}-1}{2} \\
0 & 0 & 1
\end{array}\right]\left[\begin{array}{c}
x_{\text {canonical }} \\
y_{\text {canonical }} \\
1
\end{array}\right]. \tag{8.1} ⎣ ⎡ x screen y screen 1 ⎦ ⎤ = ⎣ ⎡ 2 n x 0 0 0 2 n y 0 2 n x − 1 2 n y − 1 1 ⎦ ⎤ ⎣ ⎡ x canonical y canonical 1 ⎦ ⎤ . ( 8.1 )
请注意,该矩阵忽略了规范视图体积中点的 z 坐标,因为点沿着投影方向的距离不影响该点在图像中的投影位置。但在正式将其称为视口矩阵之前,我们添加了一行和一列来携带 z 坐标而不改变它。在本章中我们不需要它,但最终我们会需要 z 值,因为它们可以用于使更接近的表面隐藏更远的表面(参见第 9.2.3 节)。
M v p = [ n x 2 0 0 n x − 1 2 0 n y 2 0 n y − 1 2 0 0 1 0 0 0 0 1 ] . (8.2) M_{\mathrm{vp}}=\left[\begin{array}{cccc}
\frac{n_{x}}{2} & 0 & 0 & \frac{n_{x}-1}{2} \\
0 & \frac{n_{y}}{2} & 0 & \frac{n_{y}-1}{2} \\
0 & 0 & 1 & 0 \\
0 & 0 & 0 & 1
\end{array}\right]. \tag{8.2} M vp = ⎣ ⎡ 2 n x 0 0 0 0 2 n y 0 0 0 0 1 0 2 n x − 1 2 n y − 1 0 1 ⎦ ⎤ . ( 8.2 )
8.1.2 正交投影变换
正交投影变换 (Orthographic Projection Transformation)
当然,我们通常希望在规范视图体积以外的某个空间区域中渲染几何体。我们推广视图的第一步是保持固定的视角和方向,沿着 − z -z − z 方向并以 + y +y + y 为上方向,但允许查看任意矩形。我们不会替换视口矩阵,而是通过右乘另一个矩阵来扩展它。
在这些限制下,视图体积是轴对齐的盒子,我们将其边缘的坐标命名,使其视图体积为 [ l , r ] × [ b , t ] × [ f , n ] [l,r]×[b,t]×[f,n] [ l , r ] × [ b , t ] × [ f , n ] (如图 8.4 所示)。我们称此框为正交视图体积,并将边界平面称为以下内容:
x = l ≡ l e f t p l a n e , x = r ≡ r i g h t p l a n e , y = b ≡ b o t t o m p l a n e , y = t ≡ t o p p l a n e , z = n ≡ n e a r p l a n e z = f ≡ f a r p l a n e . x=l≡left plane, \\
x=r≡right plane, \\
y=b≡bottom plane, \\
y=t≡top plane, \\
z=n≡near plane \\
z=f≡far plane. x = l ≡ l e f tpl an e , x = r ≡ r i g h tpl an e , y = b ≡ b o tt o m pl an e , y = t ≡ t o ppl an e , z = n ≡ n e a r pl an e z = f ≡ f a r pl an e .
图 8.4. 正交视体 (ortho-graphic view volume)
这个词汇假定观察者沿着 − z -z − z 轴向前看,头指向 y y y 方向。这意味着 n > f n>f n > f ,这可能不太直观,但是如果您假设整个正交视图体积具有 − z -z − z 值,则当且仅当 n > f n>f n > f 时,z = n z=n z = n “近”平面靠近观察者;这里,f f f 是一个比 n n n 更小的数,即绝对值比 n n n 大的负数。
这个概念如图 8.5 所示。从正交视图体积到规范视图体积的变换是另一个窗口变换,因此我们可以简单地将正交和规范视图体积的边界代入方程(7.7)中来获得此变换的矩阵:
M orth = [ 2 r − l 0 0 − r + l r − l 0 2 t − b 0 − t + b t − b 0 0 2 n − f − n + f n − f 0 0 0 1 ] . (8.3) \mathbf{M}_{\text {orth }}=\left[\begin{array}{cccc}
\frac{2}{r-l} & 0 & 0 & -\frac{r+l}{r-l} \\
0 & \frac{2}{t-b} & 0 & -\frac{t+b}{t-b} \\
0 & 0 & \frac{2}{n-f} & -\frac{n+f}{n-f} \\
0 & 0 & 0 & 1
\end{array}\right]. \tag{8.3} M orth = ⎣ ⎡ r − l 2 0 0 0 0 t − b 2 0 0 0 0 n − f 2 0 − r − l r + l − t − b t + b − n − f n + f 1 ⎦ ⎤ . ( 8.3 )
这个矩阵非常接近 OpenGL 传统上使用的矩阵,只是 n,f 和 zcanonical 的符号相反。
图 8.5。正交视图体积沿着负 z z z 轴,因此 f f f 是比 n n n 更小的负数。因此,x=l≡左平面,x=r≡右平面,y=b≡底部平面,y=t≡顶部平面,z=n≡近平面,z=f≡远平面。
大多数程序员都认为 x x x 轴指向右,y y y 轴指向上是直观的。在右手坐标系中,这意味着我们朝 − z -z − z 方向观察。一些系统使用左手坐标系进行视图,以便注视方向沿 + z +z + z 方向。哪种方式更好是个人喜好问题,本文假设使用右手坐标系。本章末尾的注释中提供了支持左手坐标系的参考资料。
为了在正交视图体积中绘制 3D 线段,我们将它们投影到屏幕的 x x x 和 y y y 坐标并忽略 z z z 坐标。我们通过组合方程(8.2)和(8.3)来实现这一点。请注意,在程序中,我们将矩阵相乘形成一个矩阵,然后按以下方式操作点:
[ x pixel y pixel z canonical 1 ] = ( M v p M orth ) [ x y z 1 ] . \left[\begin{array}{c}
x_{\text {pixel }} \\
y_{\text {pixel }} \\
z_{\text {canonical }} \\
1
\end{array}\right]=\left(\mathbf{M}_{\mathrm{vp}} \mathbf{M}_{\text {orth }}\right)\left[\begin{array}{c}
x \\
y \\
z \\
1
\end{array}\right] \text {. } ⎣ ⎡ x pixel y pixel z canonical 1 ⎦ ⎤ = ( M vp M orth ) ⎣ ⎡ x y z 1 ⎦ ⎤ .
现在,z z z 坐标将在 [ − 1 , 1 ] [-1,1] [ − 1 , 1 ] 之间。我们现在不利用它,但在我们研究 z-buffer
算法时它将非常有用。
因此,绘制具有端点 a i a_i a i 和 b i b_i b i 的许多 3D 线段的代码变得简单而高效:
这是矩阵变换机制如何使图形程序变得简洁高效的第一个例子。
construct M v p construct M orth M = M v p M orth for each line segment ( a i , b i ) do p = M a i q = M b i drawline ( x p , y p , x q , y q ) \begin{array}{l}
\text { construct } \mathbf{M}_{\mathrm{vp}} \\
\text { construct } \mathbf{M}_{\text {orth }} \\
\mathbf{M}=\mathbf{M}_{\mathrm{vp}} \mathbf{M}_{\text {orth }} \\
\text { for each line segment }\left(\mathbf{a}_{i}, \mathbf{b}_{i}\right) \text { do } \\
\quad \mathbf{p}=\mathbf{M} \mathbf{a}_{i} \\
\mathbf{q}=\mathbf{M} \mathbf{b}_{i} \\
\text { drawline }\left(x_{p}, y_{p}, x_{q}, y_{q}\right)
\end{array} construct M vp construct M orth M = M vp M orth for each line segment ( a i , b i ) do p = M a i q = M b i drawline ( x p , y p , x q , y q )
我们希望能够在 3D 中更改视点并查看任何方向。有多种约定可用于指定观察者位置和方向。我们将使用以下约定(见图 8.6):
图 8.6。用户通过眼睛位置 e、注视方向 g 和上向量 t 指定视图。我们构造一个右手坐标系,其中 w 指向注视的相反方向,v 在 g 和 t 所在的同一平面中。
眼睛位置 e,
注视方向 g,
视角向上的矢量 t。
眼睛位置是眼睛“看到的”位置。如果您将图形视为摄影过程,则它是镜头的中心。注视方向是指观察者正在查看的任何方向的向量。视角向上的矢量是该平面内的任何矢量,该平面将观察者的头分成左右两半,并指向站在地面上的人们的天空方向。这些向量为我们提供了足够的信息来设置一个坐标系,其中原点为 e,uvw 基础,
图 8.7。对于任意视角,我们需要将要存储的点转换为“适当”的坐标系。在这种情况下,它具有以 uvw 为基底的 e 为原点和偏移坐标。
使用第 2.4.7 节的公式:
w = − g ∥ g ∥ u = t × w ∥ t × w ∥ , v = w × u . \begin{aligned}
\mathbf{w} & =-\frac{\mathbf{g}}{\|\mathbf{g}\|} \\
\mathbf{u} & =\frac{\mathbf{t} \times \mathbf{w}}{\|\mathbf{t} \times \mathbf{w}\|}, \\
\mathbf{v} & =\mathbf{w} \times \mathbf{u} .
\end{aligned} w u v = − ∥ g ∥ g = ∥ t × w ∥ t × w , = w × u .
如果我们希望转换的所有点都以 u、v 和 w 作为基向量,并相对于 e 作为原点存储在坐标中,则我们的工作即可完成。但是,如图 8.7 所示,模型的坐标是以规范(或世界)原点 o 和 x、y 和 z 轴表示的。为了使用我们已经开发的机制,我们只需要将我们希望绘制的线段端点的坐标从 xyz 坐标转换为 uvw 坐标。这种类型的变换在第 7.5 节中讨论过,实施此变换的矩阵是相机坐标系的规范到基向量矩阵:
M c a m = [ u v w e 0 0 0 1 ] − 1 = [ x u y u z u 0 x v y v z v 0 x w y w z w 0 0 0 0 1 ] [ 1 0 0 − x e 0 1 0 − y e 0 0 1 − z e 0 0 0 1 ] (8.4) \mathbf{M}_{\mathrm{cam}}=\left[\begin{array}{llll}
\mathbf{u} & \mathbf{v} & \mathbf{w} & \mathbf{e} \\
0 & 0 & 0 & 1
\end{array}\right]^{-1}=\left[\begin{array}{cccc}
x_{u} & y_{u} & z_{u} & 0 \\
x_{v} & y_{v} & z_{v} & 0 \\
x_{w} & y_{w} & z_{w} & 0 \\
0 & 0 & 0 & 1
\end{array}\right]\left[\begin{array}{cccc}
1 & 0 & 0 & -x_{e} \\
0 & 1 & 0 & -y_{e} \\
0 & 0 & 1 & -z_{e} \\
0 & 0 & 0 & 1
\end{array}\right] \tag{8.4} M cam = [ u 0 v 0 w 0 e 1 ] − 1 = ⎣ ⎡ x u x v x w 0 y u y v y w 0 z u z v z w 0 0 0 0 1 ⎦ ⎤ ⎣ ⎡ 1 0 0 0 0 1 0 0 0 0 1 0 − x e − y e − z e 1 ⎦ ⎤ ( 8.4 )
或者,我们可以将这个相同的变换看作是首先将 e 移动到原点,然后将 u、v、w 对齐到 x、y、z。
为了使我们之前仅使用 z 轴的观察算法适用于任何位置和方向的相机,我们只需要将此相机变换添加到视口和投影变换的乘积中,以便在将点投影之前将传入的点从世界坐标转换为相机坐标:
construct M v p construct M orth construct M cam M = M v p M orth M c a m for each line segment ( a i , b i ) do p = M a i q = M b i drawline ( x p , y p , x q , y q ) \begin{array}{l}
\text { construct } \mathbf{M}_{\mathrm{vp}} \\
\text { construct } \mathbf{M}_{\text {orth }} \\
\text { construct } \mathbf{M}_{\text {cam }} \\
\mathbf{M}=\mathbf{M}_{\mathrm{vp}} \mathbf{M}_{\text {orth }} \mathbf{M}_{\mathrm{cam}} \\
\text { for each line segment }\left(\mathbf{a}_{i}, \mathbf{b}_{i}\right) \text { do } \\
\quad \mathbf{p}=\mathrm{Ma}_{i} \\
\mathbf{q}=\mathrm{Mb}_{i} \\
\quad \text { drawline }\left(x_{p}, y_{p}, x_{q}, y_{q}\right)
\end{array} construct M vp construct M orth construct M cam M = M vp M orth M cam for each line segment ( a i , b i ) do p = Ma i q = Mb i drawline ( x p , y p , x q , y q )
同样,一旦矩阵基础设施就位,几乎不需要编写代码。
我们最后留下了透视,因为它需要一些巧妙的技巧才能将其与向量和矩阵变换系统融合在一起。为了看清楚我们需要做什么,让我们看一下透视投影变换需要在相机空间中对点进行的操作。回忆一下,观察点位于原点处,相机沿 z 轴向负方向望去。
暂时忽略 z 的符号,以使方程更简单,但它将在第 168 页回到我们的讨论中。
透视变换的关键属性是,在原点上朝负 z 轴望去的眼睛屏幕上的物体大小与 1/z 成比例。这可以用图 8.8 中几何的一个更精确的方程式来表示:
y s = d z y (8.5) y_s={d \over z} y \tag{8.5} y s = z d y ( 8.5 )
图 8.8。方程式(8.5)的几何形状。观察者的眼睛在 e 处,注视方向为 g(负 z 轴)。视平面距离眼睛 d 个单位。点被投影到 e 的方向,并在它与视平面相交的地方被绘制。
其中 y 是沿 y 轴的点的距离,y s y_s y s 是应该在屏幕上绘制点的位置。
我们真的想使用我们为正交投影开发的矩阵机制来绘制透视图像;然后我们只需将另一个矩阵乘入我们的复合矩阵中,并使用我们已经拥有的算法即可。但是,这种变换中输入向量的坐标之一出现在分母中,不能使用仿射变换实现。
我们可以通过对我们一直用于仿射变换的齐次坐标机制进行简单推广来允许除法。我们一致同意使用齐次向量 [ x y z 1 ] T [x y z 1]^T [ x yz 1 ] T 表示点 ( x , y , z ) (x,y,z) ( x , y , z ) ;额外的坐标 w 始终等于 1,并且这通过始终使用 [ 0001 ] T [0 0 0 1]^T [ 0001 ] T 作为仿射变换矩阵的第四行来确保。
我们不再仅仅将 1 视为额外的部分,用于强制矩阵乘法实现平移,我们现在定义其为 x、y 和 z 坐标的分母:齐次向量 [ x y z w ] T [x y z w]^T [ x yz w ] T 表示点 ( x / w , y / w , z / w ) (x/w,y/w,z/w) ( x / w , y / w , z / w ) 。当 w = 1 w=1 w = 1 时,这没有任何区别,但是如果我们允许变换矩阵的底行中的任何值,则允许实现更广泛的变换,从而使 w w w 取其他值。
具体而言,线性变换使我们能够计算如下的表达式:
x ′ = a x + b y + c z x'=ax+by+cz x ′ = a x + b y + cz
仿射变换将其扩展为
x ′ = a x + b y + c z + d 。 x'=ax+by+cz+d。 x ′ = a x + b y + cz + d 。
将 w 视为分母进一步扩大了可能性,允许我们计算如下的函数:
x ′ = a x + b y + c z + d e x + f y + g z + h ; x'={ax+by+cz+d \over ex+fy+gz+h}; x ′ = e x + f y + g z + h a x + b y + cz + d ;
这可以称为 x、y 和 z 的“线性有理函数”。但是还有一个额外的约束——变换后点的分母对于所有坐标都相同:
x ′ = a 1 x + b 1 y + c 1 z + d 1 e x + f y + g z + h , y ′ = a 2 x + b 2 y + c 2 z + d 2 e x + f y + g z + h , z ′ = a 3 x + b 3 y + c 3 z + d 3 e x + f y + g z + h 。 x'={a_1x+b1_y+c_1z+d_1 \over ex+fy+gz+h}, \\
\\
y'={a_2x+b_2y+c_2z+d_2 \over ex+fy+gz+h}, \\
\\
z'={a_3x+b_3y+c_3z+d_3 \over ex+fy+gz+h}。 x ′ = e x + f y + g z + h a 1 x + b 1 y + c 1 z + d 1 , y ′ = e x + f y + g z + h a 2 x + b 2 y + c 2 z + d 2 , z ′ = e x + f y + g z + h a 3 x + b 3 y + c 3 z + d 3 。
表示为矩阵变换,
[ x ~ y ~ z ~ w ~ ] = [ a 1 b 1 c 1 d 1 a 2 b 2 c 2 d 2 a 3 b 3 c 3 d 3 e f g h ] [ x y z 1 ] \left[\begin{array}{c}
\tilde{x} \\
\tilde{y} \\
\tilde{z} \\
\tilde{w}
\end{array}\right]=\left[\begin{array}{llll}
a_{1} & b_{1} & c_{1} & d_{1} \\
a_{2} & b_{2} & c_{2} & d_{2} \\
a_{3} & b_{3} & c_{3} & d_{3} \\
e & f & g & h
\end{array}\right]\left[\begin{array}{c}
x \\
y \\
z \\
1
\end{array}\right] ⎣ ⎡ x ~ y ~ z ~ w ~ ⎦ ⎤ = ⎣ ⎡ a 1 a 2 a 3 e b 1 b 2 b 3 f c 1 c 2 c 3 g d 1 d 2 d 3 h ⎦ ⎤ ⎣ ⎡ x y z 1 ⎦ ⎤
并且
( x ′ , y ′ , z ′ ) = ( x ~ / w ~ , y ~ / w ~ , z ~ / w ~ ) \left(x^{\prime}, y^{\prime}, z^{\prime}\right)=(\tilde{x} / \tilde{w}, \tilde{y} / \tilde{w}, \tilde{z} / \tilde{w}) ( x ′ , y ′ , z ′ ) = ( x ~ / w ~ , y ~ / w ~ , z ~ / w ~ )
这样的变换被称为投影变换或单应性变换。
示例 17 矩阵
M = [ 2 0 − 1 0 3 0 0 2 3 1 3 ] \mathbf{M}=\left[\begin{array}{ccc}
2 & 0 & -1 \\
0 & 3 & 0 \\
0 & \frac{2}{3} & \frac{1}{3}
\end{array}\right] M = ⎣ ⎡ 2 0 0 0 3 3 2 − 1 0 3 1 ⎦ ⎤
该矩阵表示一个二维投影变换,将单位正方形 ([0,1] × [0,1]) 转换为图 8.9 所示的四边形。
图 8.9 透视变换将正方形映射到四边形,保持直线但不保持平行线。
例如,正方形右下角的点 ( 1 , 0 ) (1,0) ( 1 , 0 ) 由齐次向量 [ 101 ] T [1 0 1]^T [ 101 ] T 表示,并进行如下变换:
[ 2 0 − 1 0 3 0 0 2 3 1 3 ] [ 1 0 1 ] = [ 1 0 1 3 ] \left[\begin{array}{ccc}
2 & 0 & -1 \\
0 & 3 & 0 \\
0 & \frac{2}{3} & \frac{1}{3}
\end{array}\right]\left[\begin{array}{l}
1 \\
0 \\
1
\end{array}\right]=\left[\begin{array}{c}
1 \\
0 \\
\frac{1}{3}
\end{array}\right] ⎣ ⎡ 2 0 0 0 3 3 2 − 1 0 3 1 ⎦ ⎤ ⎣ ⎡ 1 0 1 ⎦ ⎤ = ⎣ ⎡ 1 0 3 1 ⎦ ⎤
它表示点 ( 1 / 13 , 0 / 13 ) (1/13,0/13) ( 1/13 , 0/13 ) ,或者 ( 3 , 0 ) (3,0) ( 3 , 0 ) 。请注意,如果我们使用矩阵
3 M = [ 6 0 − 3 0 9 0 0 2 1 ] \mathbf{3M}=\left[\begin{array}{ccc}
6 & 0 & -3 \\
0 & 9 & 0 \\
0 & 2 & 1
\end{array}\right] 3M = ⎣ ⎡ 6 0 0 0 9 2 − 3 0 1 ⎦ ⎤
结果也是 [ 301 ] T [3 0 1]^T [ 301 ] T ,它也表示 ( 3 , 0 ) (3,0) ( 3 , 0 ) 。实际上,任何标量倍数 c M c \bold M c M 都是等效的:分子和分母都被 c c c 缩放,这不会改变结果。
有一种更优雅的方式来表达相同的思想,它避免了将 w w w 坐标视为特殊处理。在这个观点中,三维投影变换只是一个四维线性变换,额外的规定是所有向量的标量倍数引用同一点:
x ∼ α x f o r a l l α ≠ 0 。 x \sim αx \space \bold {for \space all} \space α ≠ 0。 x ∼ αx for all α = 0 。
符号 ∼ \sim ∼ 读作“等价于”,意味着两个齐次向量都描述了空间中的同一点。
图 8.10。点 x = 1.5 x = 1.5 x = 1.5 由线 x = 1.5 h x = 1.5h x = 1.5 h 上的任何点表示,例如空心圆处的点。但是,在将 x 解释为传统笛卡尔坐标之前,我们首先通过 h 进行除法,得到 ( x , h ) = ( 1.5 , 1 ) (x,h)=(1.5,1) ( x , h ) = ( 1.5 , 1 ) ,如黑点所示。
示例 18 在一维齐次坐标中,我们使用二元向量表示实数线上的点,可以使用齐次向量 [ 1.51 ] T [1.5 1]^T [ 1.51 ] T 或齐次空间中 x = 1.5 h x=1.5h x = 1.5 h 线上任何其他点来表示点 (1.5)。(见图 8.10。)
在二维齐次坐标中,我们使用三元向量表示平面上的点,可以使用齐次向量 [ − 2 ; − 1 ; 2 ] T [-2;-1;2]^T [ − 2 ; − 1 ; 2 ] T 或线 x = α [ − 1 − 0.51 ] T x=α[-1 -0.5 1]^T x = α [ − 1 − 0.51 ] T 上的任何其他点来表示点 (-1,-0.5)。(见图 8.11。)
可以随意多次转换齐次向量,而不必担心 w 坐标的值——事实上,在某些中间阶段 w 坐标为零也没问题。只有当我们想要点的普通笛卡尔坐标时,我们需要将其归一化为具有 w = 1 w=1 w = 1 的等效点,这相当于将所有坐标除以 w。完成此操作后,我们可以从齐次向量的前三个分量中读取 ( x , y , z ) (x,y,z) ( x , y , z ) 坐标。
图 8.11。齐次坐标中的点等价于通过它和原点的线上的任何其他点,将点归一化相当于将该线与平面 w = 1 w=1 w = 1 相交。
8.3 透视投影
投影变换机制使得实现所需的 z 分割以实现透视变得简单。在图 8.8 中显示的二维示例中,我们可以使用矩阵变换来实现透视投影:
[ y s 1 ] ∼ [ d 0 0 0 1 0 ] [ y z 1 ] \left[\begin{array}{c}
y_{s} \\
1
\end{array}\right] \sim\left[\begin{array}{lll}
d & 0 & 0 \\
0 & 1 & 0
\end{array}\right]\left[\begin{array}{c}
y \\
z \\
1
\end{array}\right] [ y s 1 ] ∼ [ d 0 0 1 0 0 ] ⎣ ⎡ y z 1 ⎦ ⎤
这将 2D 齐次向量 [ y ; z ; 1 ] T [y;z;1]^T [ y ; z ; 1 ] T 转换为 1D 齐次向量 [ d y z ] T [dy \space z]^T [ d y z ] T ,它表示 1D 点 ( d y / z ) (dy/z) ( d y / z ) (因为它等价于 1D 齐次向量 [ d y / z 1 ] T [dy/z \space 1]^T [ d y / z 1 ] T ) ,这符合方程 (8.5)。
对于“正式”的 3D 透视投影矩阵,我们采用通常的惯例,即摄像机位于原点,面向 − z -z − z 方向,因此点 ( x , y , z ) (x,y,z) ( x , y , z ) 的距离是 − z -z − z 。与正交投影类似,我们还采用了近和远平面的概念来限制可见距离范围。在这种情况下,我们将使用近平面作为投影平面,因此图像平面距离为 − n -n − n 。
然后,所需的映射是 y s = ( n / z ) y ys=(n/z)y ys = ( n / z ) y ,x 同理。可以通过透视矩阵 (perspective matrix) 实现此变换:
P = [ n 0 0 0 0 n 0 0 0 0 n + f − f n 0 0 1 0 ] \mathbf{P}=\left[\begin{array}{cccc}
n & 0 & 0 & 0 \\
0 & n & 0 & 0 \\
0 & 0 & n+f & -f n \\
0 & 0 & 1 & 0
\end{array}\right] P = ⎣ ⎡ n 0 0 0 0 n 0 0 0 0 n + f 1 0 0 − f n 0 ⎦ ⎤
请记住,n < 0 n<0 n < 0 。
第一、二和四行简单地实现了透视方程。第三行,与正交投影和视口矩阵一样,旨在将 z 坐标“捆绑”在一起,以便我们稍后可以用来进行隐藏表面消除。然而,在透视投影中,添加一个非常量分母会使我们无法实际保持 z 的值 —— 事实上,在让 x 和 y 做我们需要它们做的事情的同时,保持 z 不变是不可能的。相反,我们选择在近平面或远平面上保持 z 不变。
有许多矩阵可以作为透视矩阵使用,并且它们所有都会对 z 坐标进行非线性扭曲。这个特定的矩阵具有图 8.12 和图 8.13 所示的良好属性;它完全保留位于 (z=n) 平面上的点,并在适当的程度上“压缩”(squishing) (z=f) 平面上的点的 x 和 y 坐标。矩阵对点 ( x , y , z ) (x,y,z) ( x , y , z ) 的效果为
P [ x y z 1 ] = [ n x n y ( n + f ) z − f n z ] ∼ [ n x z n y z n + f − f n z 1 ] \mathbf{P}\left[\begin{array}{l}
x \\
y \\
z \\
1
\end{array}\right]=\left[\begin{array}{c}
n x \\
n y \\
(n+f) z-f n \\
z
\end{array}\right] \sim\left[\begin{array}{c}
\frac{n x}{z} \\
\frac{n y}{z} \\
n+f-\frac{f n}{z} \\
1
\end{array}\right] P ⎣ ⎡ x y z 1 ⎦ ⎤ = ⎣ ⎡ n x n y ( n + f ) z − f n z ⎦ ⎤ ∼ ⎣ ⎡ z n x z n y n + f − z f n 1 ⎦ ⎤
图 8.12。透视投影保留了 z=n 平面上的点,并将透视体后面的大矩形映射到正交视体后面的小矩形上。
图 8.13。透视投影将通过原点/眼睛的任何直线映射到与 z 轴平行且不移动在 z=n 上的该直线上的点。
如您所见,对 x 和 y 进行缩放并将它们除以 z 是非常重要的。因为 n 和 z(在视体内)都是负数,所以 x 和 y 没有“反转”。虽然这不是显然的(参见本章末尾的练习题),但变换还保持了 z = n 和 z = f 之间的 z 值相对顺序,使我们可以在应用此矩阵后进行深度排序。当我们进行隐藏表面消除时,这一点非常重要。
有时,我们希望取 P 的逆矩阵,例如将屏幕坐标加 z 返回到原始空间中,就像我们可能需要做的选取操作一样。其逆矩阵为:
P − 1 = [ 1 n 0 0 0 0 1 n 0 0 0 0 0 1 0 0 − 1 f n n + f f n ] \mathbf{P}^{-1}=\left[\begin{array}{cccc}
\frac{1}{n} & 0 & 0 & 0 \\
0 & \frac{1}{n} & 0 & 0 \\
0 & 0 & 0 & 1 \\
0 & 0 & -\frac{1}{f n} & \frac{n+f}{f n}
\end{array}\right] P − 1 = ⎣ ⎡ n 1 0 0 0 0 n 1 0 0 0 0 0 − f n 1 0 0 1 f n n + f ⎦ ⎤
由于将齐次向量乘以标量不会改变其含义,因此作用于齐次向量的矩阵也是如此。因此,我们可以通过乘以 nf 来更美观地写出逆矩阵:
P − 1 = [ f 0 0 0 0 f 0 0 0 0 0 f n 0 0 − 1 n + f ] \mathbf{P}^{-1}=\left[\begin{array}{cccc}
f & 0 & 0 & 0 \\
0 & f & 0 & 0 \\
0 & 0 & 0 & f n \\
0 & 0 & -1 & n+f
\end{array}\right] P − 1 = ⎣ ⎡ f 0 0 0 0 f 0 0 0 0 0 − 1 0 0 f n n + f ⎦ ⎤
该矩阵不是矩阵 P 的逆矩阵,但它描述的变换是由矩阵 P 描述的变换的逆变换。
在公式(8.3)中与正交投影矩阵 Morth 相关的上下文中,透视矩阵简单地将透视视图体(其形状类似于金字塔的切片或棱锥体)映射到正交视图体(其为轴对齐的盒子)。透视矩阵的美妙之处在于,一旦应用它,我们就可以使用正交变换来进入规范化视图体。因此,所有正交机制都适用,我们添加的只是一个矩阵和除以 w 的操作。也很令人欣慰的是,我们没有“浪费”四行四列的矩阵中的底行。
将 P \bold P P 与 M o r t h \bold M_{orth} M or t h 串联起来得到透视投影矩阵,
M p e r = M o r t h P \bold M_{per} = \bold M_{orth}\bold P M p er = M or t h P
然而,问题是:如何确定透视投影中的 l 、 r 、 b 、 t l、r、b、t l 、 r 、 b 、 t ?它们标识我们查看的“窗口”。由于透视矩阵不会改变 (z=n) 平面上 x 和 y 的值,因此我们可以在该平面上指定 ( l , r , b , t ) (l,r,b,t) ( l , r , b , t ) 。
为了将透视矩阵集成到我们的正交基础设施中,我们只需用 Mper 替换 Morth,在应用相机矩阵 Mcam 之后但在正交投影之前插入透视矩阵 P。因此,用于透视观察的完整矩阵集合为
M = M v p M o r t h P M c a m \bold M=\bold M_{vp}\bold M_{orth}\bold P\bold M_{cam} M = M v p M or t h P M c am
得到的算法是
c o m p u t e M v p c o m p u t e M p e r c o m p u t e M c a m M = M v p M p e r M c a m for each line segment ( a i , b i ) d o p = M a i q = M b i drawline ( x p / w p , y p / w p , x q / w q , y q / w q ) \begin{array}{l}
compute \space \bold M_{vp} \\
compute \space \bold M_{per} \\
compute \space \bold M_{cam} \\
\mathbf{M}=\mathbf{M}_{\mathrm{vp}} \mathbf{M}_{\mathbf{p e r}} \mathbf{M}_{\mathrm{cam}} \\
\text { for each line segment }\left(\mathbf{a}_{i}, \mathbf{b}_{i}\right) \mathbf{d o} \\
\mathbf{p}=\mathbf{M a}_{i} \\
\mathbf{q}=\mathbf{M} \mathbf{b}_{i} \\
\text { drawline }\left(x_{p} / w_{p}, y_{p} / w_{p}, x_{q} / w_{q}, y_{q} / w_{q}\right)
\end{array} co m p u t e M v p co m p u t e M p er co m p u t e M c am M = M vp M per M cam for each line segment ( a i , b i ) do p = Ma i q = M b i drawline ( x p / w p , y p / w p , x q / w q , y q / w q )
请注意,除了额外的矩阵之外,唯一的变化是除以齐次坐标 w w w 。
将 M p e r \bold M_{per} M p er 展开,如下所示:
M p e r = [ 2 n r − l 0 l + r l − r 0 0 2 n t − b b + t b − t 0 0 0 f + n n − f 2 f n f − n 0 0 1 0 ] \mathbf{M}_{\mathrm{per}}=\left[\begin{array}{cccc}
\frac{2 n}{r-l} & 0 & \frac{l+r}{l-r} & 0 \\
0 & \frac{2 n}{t-b} & \frac{b+t}{b-t} & 0 \\
0 & 0 & \frac{f+n}{n-f} & \frac{2 f n}{f-n} \\
0 & 0 & 1 & 0
\end{array}\right] M per = ⎣ ⎡ r − l 2 n 0 0 0 0 t − b 2 n 0 0 l − r l + r b − t b + t n − f f + n 1 0 0 f − n 2 f n 0 ⎦ ⎤
这样或类似的矩阵经常出现在文档中,当我们意识到它们通常是一些简单矩阵的乘积时,它们就不再神秘了。
例如 19 许多 API(如 OpenGL)使用与此处介绍的相同规范化视图体。它们还通常要求用户指定 n 和 f 的绝对值。OpenGL 的投影矩阵为
M OpenGL = [ 2 ∣ n ∣ r − l 0 r + l r − l 0 0 2 ∣ n ∣ t − b t + b t − b 0 0 0 ∣ n ∣ + ∣ f ∣ ∣ n ∣ − ∣ f ∣ 2 ∣ f ∣ ∣ n ∣ ∣ n ∣ − ∣ f ∣ 0 0 − 1 0 ] \mathbf{M}_{\text {OpenGL }}=\left[\begin{array}{cccc}
\frac{2|n|}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\
0 & \frac{2|n|}{t-b} & \frac{t+b}{t-b} & 0 \\
0 & 0 & \frac{|n|+|f|}{|n|-|f|} & \frac{2|f||n|}{|n|-|f|} \\
0 & 0 & -1 & 0
\end{array}\right] M OpenGL = ⎣ ⎡ r − l 2∣ n ∣ 0 0 0 0 t − b 2∣ n ∣ 0 0 r − l r + l t − b t + b ∣ n ∣ − ∣ f ∣ ∣ n ∣ + ∣ f ∣ − 1 0 0 ∣ n ∣ − ∣ f ∣ 2∣ f ∣∣ n ∣ 0 ⎦ ⎤
其他 API 将 n n n 和 f f f 分别设为 0 0 0 和 1 1 1 。Blinn(1996 年)建议使规范化视图体为 [ 0 , 1 ] 3 [0,1]^3 [ 0 , 1 ] 3 ,以提高效率。所有这些决策都会略微更改投影矩阵。
8.4 透视变换的一些特性
透视变换的一个重要特性是它将直线映射到直线,将平面映射到平面。此外,它将视图体中的线段映射到规范化体中的线段。为了看到这一点,考虑线段
q + t ( Q − q ) \mathbf{q}+t(\mathbf{Q}-\mathbf{q}) q + t ( Q − q )
当由 4×4 矩阵 M 变换时,它是一个可能具有不同齐次坐标的点:
M q + t ( M Q − M q ) ≡ r + t ( R − r ) \mathbf{M q}+t(\mathbf{M Q}-\mathbf{M q}) \equiv \mathbf{r}+t(\mathbf{R}-\mathbf{r}) Mq + t ( MQ − Mq ) ≡ r + t ( R − r )
齐次化的 3D 线段为
r w r + f ( t ) ( R w R − r w r ) (8.6) \frac{\mathbf{r}}{w_{r}}+f(t)\left(\frac{\mathbf{R}}{w_{R}}-\frac{\mathbf{r}}{w_{r}}\right) \tag{8.6} w r r + f ( t ) ( w R R − w r r ) ( 8.6 )
如果上式可以改写成形式
r + t ( R − r ) w r + t ( w R − w r ) (8.7) \frac{\mathbf{r}+t(\mathbf{R}-\mathbf{r})}{w_{r}+t\left(w_{R}-w_{r}\right)} \tag{8.7} w r + t ( w R − w r ) r + t ( R − r ) ( 8.7 )
则所有齐次化的点都在一条 3D 线上。对公式(8.6)进行粗暴的操作可以得到这样的形式
f ( t ) = w R t w r + t ( w R − w r ) (8.8) f(t)=\frac{w_{R} t}{w_{r}+t\left(w_{R}-w_{r}\right)} \tag{8.8} f ( t ) = w r + t ( w R − w r ) w R t ( 8.8 )
它还表明线段确实映射到保留点顺序的线段上(参见练习 8);即它们不会被重新排序或“撕裂”。
变换将线段映射到线段的副产品是,它将三角形的边缘和顶点映射到另一个三角形的边缘和顶点。因此,它将三角形映射到三角形,将平面映射到平面。
8.5 视场 (Field-of-View)
虽然我们可以使用 ( l , r , b , t ) (l,r,b,t) ( l , r , b , t ) 和 n n n 值指定任何窗口,但有时我们希望拥有一个更简单的系统,其中我们从窗口中心查看。这意味着约束条件为
l = − r , b = − t . l = -r, \\
b=-t. l = − r , b = − t .
如果我们还添加像素是正方形的约束条件,即图像没有形状畸变,则 r r r 与 t t t 的比必须与水平像素数与垂直像素数之比相同:
n x n y = r t 。 {n_x \over n_y}={r \over t}。 n y n x = t r 。
图 8.14 视场角θ是从眼睛测量到屏幕顶部的角度。
一旦指定了 n x n_x n x 和 n y n_y n y ,就只剩下一个自由度。这通常使用图 8.14 中表示为 θ θ θ 的视场来设置。有时将其称为垂直视场,以区别于左右侧之间的角度或对角线角度。从图中我们可以看出,
t a n θ 2 = t ∣ n ∣ . tan{θ \over 2}={t \over |n|}. t an 2 θ = ∣ n ∣ t .
如果已指定 n n n 和 θ θ θ ,则可以推导出 t t t 并使用更通用的视图系统的代码。在某些系统中,n n n 的值被硬编码为某个合理值,因此我们少了一个自由度。
常见问题
正交投影在实践中有用吗?
它在需要进行相对长度判断的应用程序中非常有用。此外,当透视法太昂贵时,正交投影还可以简化问题,例如在某些医学可视化应用程序中。
我用透视法绘制的分形球体看起来像椭圆形。这是一个错误吗?
不是的,这是正确的行为。如果您将眼睛放在与视口中的虚拟观察者相同的相对位置,则这些椭圆将看起来像圆形,因为它们本身是以角度查看的。
透视矩阵是否将负 z 值转换为具有相反顺序的正 z 值?这会引起麻烦吗?
是的。变换后的 z 的方程式为:
z ′ = n + f − f n z . z'=n+f−{fn \over z}. z ′ = n + f − z f n .
因此,z = + z = + z = + 被转换为 z = − ∞ z = -\infin z = − ∞ ,z = − ϵ z = -\epsilon z = − ϵ 被转换为 z = ∞ z = \infin z = ∞ 。因此,任何跨越 z = 0 z = 0 z = 0 的线段都将被“撕裂”,尽管所有点都将投影到适当的屏幕位置上。当所有对象都包含在视图体积中时,这种撕裂是不相关的。这通常通过剪辑到视图体积来确保。然而,剪切本身由于撕裂现象而变得更加复杂,这在第 9 章中讨论过。
透视矩阵改变了齐次坐标的值,这不会使移动和缩放变换不再正常工作吗?
对一个齐次点应用平移变换,我们有
其他变换也有类似的效果(见练习 5)。
注释
大部分关于视图矩阵的讨论都基于《实时渲染》(Akenine-Möller, Haines 和 Hoffman,2008)、《OpenGL 编程指南》(Shreiner 等,2004)、《计算机图形学》(Hearn 和 Baker,1986)以及《3D 游戏引擎设计》(Eberly,2000)这些参考资料。
练习
构造一个视口矩阵,用于一个系统,其中像素坐标从图像的顶部开始倒数计数,而不是从底部开始正数计数。
将视口矩阵和正交投影矩阵相乘,并证明可以通过单次应用方程式(7.7)来获得结果。
从在近平面和远平面上点的 z 值保持不变的约束条件中推导出方程式(8.3)的第三行。
代数方法证明透视矩阵在视图体积内保持 z 值的顺序。
对于一个顶部三行任意,底部一行为(0,0,0,1)的 4×4 矩阵,证明经同次化后点(x,y,z,1)和(hx,hy,hz,h)变换到相同的点。
验证文本中给出的 M-1 p 的形式是正确的。
验证完整的透视到规范矩阵 Mper 将(r,t,n)映射到(1,1,1)。
写出一个在 n = 1,f = 2 时的透视矩阵。
对于点 p = (x,y,z,1),通过练习 6 中的透视矩阵变换,它的同次化和非同次化结果分别是什么?
对于眼睛位置 e =(0,1,0),注视向量 g =(0,–1,0)和视图向上向量 t =(1,1,0),得到用于坐标旋转的正交 uvw 基础是什么?
证明对于透视变换,从视图体积开始的线段在同次化后会映射到规范体积内的线段。此外,证明两个线段上的点的相对顺序是相同的。提示:展示方程式(8.8)中的 f(t)具有 f(0)= 0,f(1)= 1,f 的导数在 [0,1] 内的所有 t 上都是正的,并且同次坐标不会改变符号。