现在,我们已经了解了点和矩阵变换的基础,我们可以来描述它们是如何应用在计算机图形学中的。我们会讨论各种模型操作和图像操作。
5.1 世界,物体和视角坐标系
当我们在场景中描述几何体时,我们开始于一个基本右手正交坐标系w ⃗ t \vec{\mathbf{w}}^t w t ,称为世界坐标系(world frame) 。我们永远不会改变世界坐标系。其他坐标系可以随后相关于这一世界坐标系来描述。如果我们使用相关于世界坐标系的坐标系向量来表示某些点的位置,它们就被称为世界坐标系向量(world coordinates) 。
假设我们希望在场景中建立一个移动车辆的模型。我们希望使用顶点坐标系来建立模型,不需要关心该物体在场景全局中的摆放位置。同样,我们也想在场景中移动车辆而不需要对模型的坐标系向量进行改动。这些通过物体坐标系来完成。
对于场景中的每一个物体,我们都与之关联一个右手正交物体坐标系o ⃗ t \vec{\mathbf{o}}^t o t 。我们现在可以使用与物体坐标系统相关的坐标系向量表示一个物体部分的位置。这些坐标系被称作物体坐标系(object coordinates) 并存储在我们的计算机程序中。想要移动整个物体,我们简单的更新o ⃗ t \vec{\mathbf{o}}^t o t ,而不需要改变任何我们描述点的坐标系向量。
物体坐标系与世界坐标系的关系由仿射 4x4 (刚体)矩阵表示,我们称之为O O O 。则有,
o ⃗ t = w ⃗ t O \vec{\mathbf{o}}^t =\vec{\mathbf{w}}^tO o t = w t O
在程序中,我们会存储矩阵O O O ,使用上述等式,矩阵O O O 能够将世界坐标系与物体坐标系统相关联。想要移动坐标系o ⃗ t \vec{\mathbf{o}}^t o t ,我们改变矩阵O O O 。
在现实世界中,当我们想要从 3D 环境中创建一张 2D 图像时,我们将一个相机放置在场景中的某处。每个物体在图像中的位置,与它在 3D 中与相机的关系有关(即在某一合适的基向量下它们的坐标系向量)。
在计算机图形学中,我们使用一个右手性的正交坐标系e ⃗ t \vec{\mathbf{e}}^t e t ,称作视角坐标系。我们将视线解读为沿着它的z轴负方向并创建一张图像(如图5.1)。视角坐标系由某一 4x4 (刚体)矩阵E E E 来描述,
e ⃗ t = w ⃗ t E \vec{\mathbf{e}}^t =\vec{\mathbf{w}}^tE e t = w t E
图5.1 :世界坐标系为红色,物体坐标系为绿色,而视角坐标系为蓝色。视角沿着其 z 轴负方向向向下看向物体。
在计算机程序中,我们会显式的存储矩阵E E E 。
给定一点
p ~ = o ⃗ t c = w ⃗ t O c = e ⃗ t E − 1 O c \tilde{p} =\vec{\mathbf{o}}^t\mathbf{c} =\vec{\mathbf{w}}^tO\mathbf{c} =\vec{\mathbf{e}}^tE^{-1}O\mathbf{c} p ~ = o t c = w t O c = e t E − 1 O c ,
我们将c \mathbf{c} c 称作物体坐标系向量,O c O\mathbf{c} O c 为世界坐标系向量,而E − 1 O c E^{-1}O\mathbf{c} E − 1 O c 为视角坐标系向量。我们使用脚标o o o 来表示物体坐标系向量,脚标w w w 表示世界坐标系向量,而脚标e e e 来表示视角坐标系向量。由此我们可以写出
[ x e y e z e 1 ] = E − 1 O [ x o y o z o 1 ] \left[ \begin{matrix} x_e \\ y_e \\ z_e \\ 1 \end{matrix} \right] = E^{-1}O \left[ \begin{matrix} x_o \\ y_o \\ z_o \\ 1 \end{matrix} \right] x e y e z e 1 = E − 1 O x o y o z o 1
最终,是这个视角坐标系向量指定了每一个顶点出现在渲染图像的何处。因此,如第 6 章中所述,我们的渲染过程需要对每一个顶点计算其视角坐标系向量。
5.2 移动
在一个交互式 3D 程序中,我们常常想要使用某种刚体变换在空间中移动物体以及视角。我们现在来讨论通常是如何做到这一点的。
5.2.1 移动物体
我们通过更新其对应的坐标系来移动物体,即我们更新表示它的矩阵O O O 。
假设我们希望将一个在某一坐标系a ⃗ t = w ⃗ t A \vec{\mathbf{a}}^t=\vec{\mathbf{w}}^tA a t = w t A 下表示的变换M M M 应用于一个物体坐标系o ⃗ t \vec{\mathbf{o}}^t o t 。那么,参考等式(4.1),我们有
因此我们在代码中将其实现为O ← A M A − 1 O O\leftarrow AMA^{-1}O O ← A M A − 1 O 。
如何来选择a ⃗ t \vec{\mathbf{a}}^t a t ?最明显的选择为在o ⃗ t \vec{\mathbf{o}}^t o t 下对o ⃗ t \vec{\mathbf{o}}^t o t 自身进行变换。不幸的是,这就意味着用到的轴向只会跟物体本身有关。"向右"将会是物体自身的右侧,这个方向与最终图像中的任何方向都无关。我们或许会修改这一答案,使用e ⃗ t \vec{\mathbf{e}}^t e t 来对o ⃗ t \vec{\mathbf{o}}^t o t 进行变换。这将修正轴向的问题,但是会带来另一个问题。当我们旋转物体时,它会绕着视角坐标系的中心旋转;看起来它会围绕着视角旋转。但我们发现让物体沿着其自身的中点旋转更加自然。即我们想让它绕着o ⃗ t \vec{\mathbf{o}}^t o t 的中心点旋转(如图5.2)。
图5.2 :当我们将鼠标向右移动时,我们希望物体在其自身中心围绕视角的y轴向旋转。
为了解决两个问题,我们可以创建一个新的坐标系,原点与物体坐标系相同,而轴向方向与视角坐标系相同,为了做到这一点,我们将矩阵分解如下:
O = ( O ) T ( O ) R O = (O)_T (O)_R O = ( O ) T ( O ) R
E = ( E ) T ( E ) R E=(E)_T(E)_R E = ( E ) T ( E ) R ,
其中( A ) T (A)_T ( A ) T 是A A A 的位移部分而( A ) R (A)_R ( A ) R 是旋转部分,正如等式(3.3)。我们可以得出理想的辅助坐标系为
从世界坐标系开始(从左向右读上述等式,即连续从物体空间解读)先位移至物体坐标系的中心,再将其旋转至视角坐标系方向(如图5.3)。
图5.3 :辅助坐标系a ⃗ \vec{\mathbf{a}} a 与o ⃗ \vec{\mathbf{o}} o 原点相同而与e ⃗ \vec{\mathbf{e}} e 轴向相同。x 轴向指向画面外因此被压缩了。
因此,对于这种类型的物体运动,等式中的矩阵A A A 应为A = ( O ) T ( E ) R A=(O)_T(E)_R A = ( O ) T ( E ) R 。
还有另一种计算方式可以达到相同的效果。假设,例如,我们希望沿着一个物体自身的中心旋转一个物体,而旋转轴向是k ⃗ \vec{k} k ,它与e ⃗ t \vec{\mathbf{e}}^t e t 的关系由k \mathbf{k} k 来表示。(早些时候,我们使用由k \mathbf{k} k 来获取到的矩阵M M M ,并且使用相应的A A A ,我们更新了O ← A M A − 1 O O\leftarrow AMA^{-1}O O ← A M A − 1 O 。)现在,我们可以首先计算k ′ \mathbf{k}^\prime k ′ ,即k ⃗ \vec{k} k 关于o ⃗ t \vec{\mathbf{o}}^t o t 的坐标系向量。之后将k ′ \mathbf{k}^\prime k ′ 代入到等式(2.5),我们就可以获得能够直接在o ⃗ t \vec{\mathbf{o}}^t o t 下表示该旋转的矩阵M ′ M^\prime M ′ 。在这种情况下,我们可以将物体的矩阵更新为
5.2.2 移动视角
我们可能会想进行的另一个操作是移动视角来获得不同的观察结果。这需要改变e ⃗ t \vec{\mathbf{e}}^t e t ,在我们的程序中即为更新矩阵E E E 。同样,我们必须选择一个方便的坐标系,以它来进行更新,就像我们在变换物体时所做的那样。
一个选择是使用与之前相同的辅助坐标系统。此时,视角会沿着物体的中心旋转。
另一个有用的选择是以视角自身的坐标系来变换e ⃗ t \vec{\mathbf{e}}^t e t 。这模仿了自运动,就像人们转头一样,常用于控制第一人称运动。在这种情况下,矩阵E E E 会被更新为
E ← E M ′ E \leftarrow EM^\prime E ← E M ′
5.2.3 注视(Lookat)
有时(尤其是对静止画面来说)描述视角坐标系e ⃗ t = w ⃗ t E \vec{\mathbf{e}}^t=\vec{\mathbf{w}}^tE e t = w t E 时,直接指定视角的位置p ~ \tilde{p} p ~ ,一个视角直接注视的点q ~ \tilde{q} q ~ ,以及一个"(up vector)指向上的向量"u ⃗ \vec{u} u 来描述视角的垂直方向会比较方便。这些点和向量由p \mathbf{p} p ,q \mathbf{q} q 和u \mathbf{u} u 来给出,它们的坐标系向量相对于w ⃗ t \vec{\mathbf{w}}^t w t 来描述。给定了这些输入,令
z = n o r m a l i z e ( q − p ) \mathbf{z} = normalize(\mathbf{q}-\mathbf{p}) z = n or ma l i ze ( q − p )
y = n o r m a l i z e ( u ) \mathbf{y} = normalize(\mathbf{u}) y = n or ma l i ze ( u )
x = y × z \mathbf{x} = \mathbf{y}\times \mathbf{z} x = y × z ,
其中
n o r m a l i z e ( c ) = c 1 2 + c 2 2 + c 3 2 normalize(\mathbf{c}) = \sqrt{c_1^2+c_2^2+c_3^2} n or ma l i ze ( c ) = c 1 2 + c 2 2 + c 3 2
则矩阵E E E 定义为
[ x 1 y 1 z 1 p 1 x 2 y 2 z 2 p 2 x 3 y 3 z 3 p 3 0 0 0 1 ] \left[ \begin{matrix} x_1 & y_1 & z_1 & p_1 \\ x_2 & y_2 & z_2 & p_2 \\ x_3 & y_3 & z_3 & p_3 \\ 0 & 0 & 0 & 1 \end{matrix} \right] x 1 x 2 x 3 0 y 1 y 2 y 3 0 z 1 z 2 z 3 0 p 1 p 2 p 3 1
5.3 缩放
目前为止,我们将我们的世界看作由可移动物体组成,每个物体有其自己的垂直坐标系,由其刚体矩阵o ⃗ t = w ⃗ t O \vec{\mathbf{o}}^t=\vec{\mathbf{w}}^tO o t = w t O 表示。我们将注意力限制在标准正交坐标系上,因此位移和旋转矩阵都可以按照我们期望的那样运作。
当然,对于给物体建模,我们也会希望进行缩放操作。例如,我们也许想利用积压球体来得到椭球体。一种方法是对于物体同时保存一个独立的缩放矩阵O ′ O^\prime O ′ 。则缩放后的(非标准正交)物体坐标系为o ′ ⃗ t = o ⃗ t O ′ \vec{\mathbf{o}^\prime}^t=\vec{\mathbf{o}}^tO^\prime o ′ t = o t O ′ 。通过这种方法,我们仍旧可以通过上述更新其O O O 矩阵的方式来移动物体。绘制物体时,我们使用矩阵E − 1 O O ′ E^{-1}OO^\prime E − 1 O O ′ 来变换"缩放后的物体坐标系向量"为视角坐标系向量。
5.4 层级关系(Hierarchy)
通常,将一个物体看作由一些子物体组成会比较方便,这些子物体或固定或可以移动。每个子物体或许会有其自身的标准正交坐标系,称其为a ⃗ t \vec{\mathbf{a}}^t a t (及其缩放坐标系)。随后,我们可以将子物体的顶点坐标系向量存储在其自身坐标系下。有了这种层级关系,我们希望能够简单的描述物体的整体运动及其子物体的独立运动。
例如,当建模一个肢体可动的机器人时,我们或许会使用一个物体坐标系以及一个缩放的物体坐标系来表示躯干,一个子物体坐标系表示一个可旋转的肩部,一个经过缩放的子坐标系表示上臂(随肩部一起运动)(如图 5.4)。
图5.4 :在本例中,绿色坐标系为物体坐标系o ⃗ t = w ⃗ t O \vec{\mathbf{o}}^t=\vec{\mathbf{w}}^tO o t = w t O ,而灰色坐标系为缩放后的物体坐标系o ′ ⃗ t = o ⃗ t O ′ \vec{\mathbf{o}^{\prime}}^t=\vec{\mathbf{o}}^tO^{\prime} o ′ t = o t O ′ 。一个单位立方体的坐标系向量经o ′ ⃗ t \vec{\mathbf{o}^{\prime}}^t o ′ t 变换后构成了一个长方体。改变矩阵O O O 可以用来移动整个机器人。蓝绿色的坐标系a ⃗ t = o ⃗ t A \vec{\mathbf{a}}^t=\vec{\mathbf{o}}^tA a t = o t A 是右侧肩部的坐标系。改变其中旋转要素A A A 可以用来旋转整个右臂。红色的坐标系b ⃗ t = a ⃗ t B \vec{\mathbf{b}}^t=\vec{\mathbf{a}}^tB b t = a t B 是右上侧手臂的坐标系。浅蓝色的坐标系b ′ ⃗ t = b ⃗ t B ′ \vec{\mathbf{b}^\prime}^t=\vec{\mathbf{b}}^tB^\prime b ′ t = b t B ′ 是缩放后的右上臂坐标系。一个单位球体经过b ′ ⃗ t \vec{\mathbf{b}^\prime}^t b ′ t 变换后形成了右上臂的椭球体。c ⃗ t = b ⃗ t C \vec{\mathbf{c}}^t=\vec{\mathbf{b}}^tC c t = b t C 是右侧的肘部坐标系。改变其中旋转要素C C C 可以旋转小臂。d ⃗ t = c ⃗ t D \vec{\mathbf{d}}^t=\vec{\mathbf{c}}^tD d t = c t D 和d ⃗ t = d ⃗ t D ′ \vec{\mathbf{d}}^t=\vec{\mathbf{d}}^tD^\prime d t = d t D ′ 分别是一个标准正交和缩放后的坐标系,用于绘制小臂。坐标系f ⃗ t = o ⃗ t F \vec{\mathbf{f}}^t=\vec{\mathbf{o}}^tF f t = o t F 是左侧肩部的坐标系。
当我们移动整个物体时,通过更新其矩阵O O O ,我们希望能够让所有的子物体一起移动(如图5.5)。为了得到这样的效果,我们使用一个刚体矩阵来表示子物体坐标系与物体坐标系之间的关系(而不是将子物体坐标系与世界坐标系相关联)。因此,我们存储一个刚体矩阵A A A ,用于定义一组关系a ⃗ t = o ⃗ t A \vec{\mathbf{a}}^t=\vec{\mathbf{o}}^tA a t = o t A ,以及一个缩放矩阵A ′ A^{\prime} A ′ ,用于定义子物体的缩放后坐标系为a ′ ⃗ t = a ⃗ t A ′ \vec{\mathbf{a}^\prime}^t = \vec{\mathbf{a}}^tA^\prime a ′ t = a t A ′ 。需要移动子物体的位置时,只需要更新矩阵A A A 即可。绘制子物体时,我们使用矩阵E − 1 O A A ′ E^{-1}OAA^{\prime} E − 1 O A A ′ ,它会将“缩放后的子物体坐标系向量”变换为视角坐标系向量。很明显,这个理念可以递归嵌套,我们可以用b ⃗ = a ⃗ t B \vec{\mathbf{b}}=\vec{\mathbf{a}}^tB b = a t B 来表示一个子物体的子物体,用b ′ ⃗ = a ⃗ t B ′ \vec{\mathbf{b}^\prime}=\vec{\mathbf{a}}^tB^{\prime} b ′ = a t B ′ 来表示经过缩放的子物体的子物体。
图5.5 :整体的移动机器人时,我们更新其矩阵O O O 。
在我们机器人的例子当中,我们使用了a ⃗ t \vec{\mathbf{a}}^t a t 作为右肩的坐标系,b ′ ⃗ t \vec{\mathbf{b}^\prime}^t b ′ t 作为左上臂的坐标系,以及c ⃗ t = b ⃗ t C \vec{\mathbf{c}}^t=\vec{\mathbf{b}}^tC c t = b t C 作为右肘部坐标系。d ⃗ t = c ⃗ t D \vec{\mathbf{d}}^t=\vec{\mathbf{c}}^tD d t = c t D 与d ′ ⃗ t = c ⃗ t D ′ \vec{\mathbf{d}^\prime}^t=\vec{\mathbf{c}}^tD^\prime d ′ t = c t D ′ 分别是,正交和缩放后的右小臂坐标系。需要移动整个机器人时,我们更新它的矩阵$O$(图5.5)。需要在肩膀处弯曲右臂时,我们更新矩阵A A A (图5.6)。需要在右肘部弯曲右臂时,我们更新矩阵C C C (图5.7)。
图5.6 :需要在肩膀处弯曲右臂时,我们更新矩阵A A A
图5.7 :需要弯曲右肘时,我们更新矩阵C C C
通常,我们会使用一个矩阵栈(matrix stack) 数据结构来追踪用于绘制当前子物体的矩阵。在这个矩阵栈中,push(M) 操作创建一个新的“最顶层”矩阵,它是之前顶部矩阵的一个复制。随后,用我们矩阵参数M M M 右乘这个新的最顶层矩阵。pop() 操作移除矩阵栈中的最顶层矩阵。当向一个子物体深入时,使用push 操作。当我们从底层向父层级返回时,该矩阵从栈中取出。
例如,用伪代码表示,我们或许会这样绘制上述机器人
Copy ...
matrixStack. initialize ( inv (E));
matrixStack. push (O);
matrixStack. push (O ');
draw(matrixStack.top(), cube); // body
matrixStack.pop(); // O'
matrixStack. push (A);
matrixStack. push (B);
matrixStack. push (B ');
draw(matrixStack.top(), sphere); // upper arm
matrixStack.pop(); // B'
matrixStack. push (C);
matrixStack. push (C ');
draw(matrixStack.top(), sphere); // lower arm
matrixStack.pop(); // C'
matrixStack. pop (); // C
matrixStack. pop (); // B
matrixStack. pop (); // A
// current top matrix is inv(E)*O
// we can now draw another arm
matrixStack. push (F);
...
这种层级关系可以被硬编码至程序中,如上述伪代码表示的一样,或者用某种称为场景图(scene graph) 的链接树形数据结构来表示。
练习
5.1 假设我们的场景中有一个喷气式飞机在天空中飞行。假设飞机的几何体在飞机自身坐标系j ⃗ t \vec{\mathbf{j}}^t j t 下描述,定义为j ⃗ t = w ⃗ t J \vec{\mathbf{j}}^t=\vec{\mathbf{w}}^tJ j t = w t J 。这个坐标系中心位于驾驶座舱,负z z z 轴方向指向前方的窗户外。假设我们想要从飞行员的视点渲染场景。给定一个在其他物体上的点,o ⃗ t c \vec{\mathbf{o}}^t\mathbf{c} o t c ,我们传递给渲染器来绘制这一点的坐标系向量应该是什么?
5.2 假设我们有一个图 5.4 中的机器人。假设我们想在一个原点位于骨骼中心但是轴向与视角坐标系相同的坐标系下旋转肩关节。要如何实现?(或许用等式 5.3 中的方法来完成更容易些。)