"What if angry vectors veer Round your sleeping head, and form. There’s never need to fear Violence of the poor world’s abstract storm."—Robert Penn Warren。(Patrick:这句话没啥好翻译的,挺有意思的。。)

Transform是一种操作,针对点、向量或者颜色,然后以某种方式进行转换。对于计算机图形从业者来说,掌握transform是非常重要的。通过它们,可以对物件、灯光、相机进行定位、重塑、触发动画。还可以通过它们,来将所有的计算都搬到同一个坐标系下进行,还可以将物件以各种方式进行投影。这些只是通过transform执行的一部分操作,但是已经足以证明transform在实时图形,甚至任何类型的计算机图形中的重要性。

线性的transform需要满足如下两个条件:

f (x) + f (y) = f (x + y); (4.1)

kf (x) = f (kx): (4.2)

举个例子,f(x)=5x是一个将向量每个元素都乘以5的操作。为了证明这是线性的,以上两个条件都需要满足,第一个条件成立,因为任何两个向量乘以5之后再相加,与相加后再乘以5的结果相同。第二个条件显然是满足的。这个公式被称为缩放transform,因为它只改变一个物件的缩放尺寸。旋转transform也是一个线性transform,用于将一个向量绕原点旋转。缩放和旋转transform,以及所有的针对三元素向量的线性transform,都可以通过3*3矩阵表示。

然而,这个尺寸的矩阵通常不够大。比如,f(x)= x + (7,3,2)就不是一个线性transform。按照第一个条件来算,等号坐标比等号右边多加了一次(7,3,2)。将一个固定的向量与另外一个向量相加的操作叫做平移。这个transform很有用,而且我们也经常将transform进行组装使用。比如先缩放,然后移动。如果还使用3*3矩阵的这种形式的话,将不利于组装。

可以通过仿射变换将线性变换和平移组装在一起,通常存储为4*4矩阵。一个放射变化通常是先执行一个线性transform,然后平移。我们使用同构表示法表示四元素向量,以相同的方式表示点和方向。方向向量表示为V=(Vx,Vy,Vz,0),点表示为V=(Vx,Vy,Vz,1)。在本章中,我们将广泛使用realtimerendering.com上课下载线性代数附录中解释的术语和操作。

所有的平移、旋转、缩放、反射、剪切都是仿射矩阵。仿射矩阵的特点是保持线段的平行性,但是不保证长度和角度(Patrick:这一块似懂非懂。。)。仿射矩阵之间还可以相乘。

本章从最基本的仿射变化开始。本节可以被当作这个transform的参考手册。之后会聊更多的特殊矩阵,然后讨论和描述四元数,一个强大的transform工具。在之后会聊顶点混合和变形,两种简单有效的表达mesh动画的方式。最后,会聊投影矩阵。表4.1总结了这些transform、符号、函数和属性,其中正交矩阵的逆矩阵就是转置矩阵。

T04

Transform是操作几何图形的基本工具。大多数图形API允许用户设置任意矩阵,还有一些库用于本章中将要描述的一些矩阵操作。但是理解这些API背后的矩阵变化依然很重要。首先理解这个API背后是做什么的,然后这样做的意义是什么。例如,这样的理解可以使得你再处理正交矩阵时,能够知道它是正交矩阵,然后通过转置矩阵代替逆矩阵的方式,加速代码。

4.1 Basic Transforms

本节将介绍最基本的transform,比如平移、旋转、缩放、shearing等各种transform、刚体transform、法线transform、逆运算等。对于经验丰富的读者,本节可以作为简单transform的参考手册,对于新手来说,本节将为这个主题的详细介绍。本节是本章其余部分以及本书其他章节的必要基础。下面我们从最简单的transform开始。

4.1.1 平移

平移矩阵T用于实现将一个物件从一个位置移动到另外一个位置。该矩阵用于将一个物件根据向量t=(tx,ty,tz)进行平移。矩阵T如下所示:

T04

如图4.1为平移矩阵的实例。可以看到将点P=(px、py、pz,1)与T(t)相乘,会生成一个新的点P0=(px+tx,py+yz,pz+tz,1),这就是平移。需要注意的是方向vector v=(vz,vy,vz,0)与T相乘的时候不会产生任何印象,因为方向vector不会被平移。然后,点和方向都会受到仿射变换其他部分的影响。平移矩阵的逆矩阵T-1(t)=T(-t)。

T04

上图中,左侧的方块被通过平移矩阵T(5,2,0)进行处理,然后方块被向右移动了5个单位,向上移动了2个单位。

还有一点需要提到的是,CG行业经常会看到矩阵的平移向量保存在最下面一行。比如DX就是这样的,这是因为这些向量和矩阵都是以行存储。而在本书中,我们以列为存储。这些只是单纯的表示方式不同而已,被存储在内存中的矩阵,16个数字中的最后4个永远都是3个平移数值加上1。

4.1.2 旋转

旋转transform是将一个向量(位置或者方向)沿着一个穿过原点的轴,以指定角度进行旋转。和平移矩阵一样,是一个rigid-body transform,比如,它保持了变换点之间的距离,并且不会导致左右交换边。平移矩阵和旋转矩阵在CG领域中对物件进行定位和定向非常有用。方向矩阵是旋转矩阵的一种,与相机、物件相关联,用于展示其在空间中的方向,即向上或者向前的方向(模型坐标系)。

2D中的旋转矩阵很容易就可以计算得到。比如我们有个向量 V = (Vx,Vy),我们可以这样写:V = (Vx,Vy) = (rCosΘ,rSinΘ),如果我们将该向量逆时针旋转∅,我们就能得到 u = (rCos(Θ + ∅), rSin(Θ + ∅)),解析如下:

T04

在3D领域,旋转矩阵为Rx(∅)、Ry(∅)、Rz(∅),也就是分别沿着x、y、z轴旋转∅。公式如下:

T04

如果将上述4*4矩阵的最下行和最右列删除,变成一个3*3的矩阵。针对旋转角度∅的三个3*3矩阵的对角线之和与旋转轴无关,是一个常数,即:tr(R)= 1 + 2Cos∅(参考文献[997])。

旋转矩阵的实施过程见图4.4,旋转矩阵的特征有2个,一是绕i轴旋转,二是保持旋转轴i上所有点不变。需要注意的是,旋转矩阵也可以用于表示绕任意轴的旋转矩阵,上面给出的旋转轴矩阵可以用于一系列旋转,以执行任意轴旋转。这些将在4.2.1和4.2.4中讨论。

上图为绕着特定点P旋转。

所有的旋转矩阵的行列式都为1,且为正交矩阵。这样的话可以将数个transform连接在一起。还可以通过以下方式来表示旋转矩阵的逆:R-1(∅) = R(-∅),绕着同一个轴进行反方向旋转。

下面看一个例子:绕着p点旋转。假设我们想绕着z轴旋转,旋绕的中心点为P,步骤如图4.2。由于绕着一个点旋转的时候,这个点不动,所以我们先通过T(-P)将物件移动到零点,然后执行旋转操作,最后再通过T(P)把物件移动回来。所以transform X = T(P)R(∅)T(-P),需要注意上述矩阵的顺序。

T04

4.1.3 缩放

缩放矩阵,S(s)= S(Sx,Sy,Sz)将一个物件针对x、y、z轴按照Sx、Sy、Sz的比例进行缩放。也就是说缩放矩阵可以放大或者缩小物件。缩放因子Si越大,该轴向的缩放比例越大。如果缩放因子为1,那么该方向就不会进行缩放。下面为缩放矩阵:

T04

图4.4展示了缩放矩阵的作用。如果三个轴向的缩放因子相同,则被称为均匀uniform缩放操作,否则,则被称为非均匀nonuniform缩放操作。有时候,也被称为各向同性isotropic和各向异性anisotropic。逆矩阵S-1(s)= S(1/Sx,1/Sy,1/Sz)

当使用齐次坐标系的时候,还有一种表示均匀uniform缩放的矩阵,就是利用矩阵的右下角的数值。该数值会影响齐次坐标系中的w值,然后通过这个值可以影响到点的每个轴向的数值(均匀缩放对方向向量无效,所以不考虑)。比如,一个比例为5的缩放矩阵可以用如下两种矩阵表示:

T04

然而对比这两个矩阵,S’必须要做homogenization操作。这个可能会比较低效,因为在做homogenization的时候引入了除法的操作,如果右下角的数值为1,则不会进行除法操作。然而,如果某个系统不会去判断是否为1,而一直进行除法操作,则就不会有多余的消耗。

当一个或者三个缩放因子为负数时,被称为reflection矩阵,或者mirror矩阵。如果两个缩放因子为-1,则该物件被旋转了Π。需要注意的是,如果一个旋转矩阵乘以一个reflection矩阵,得到的依然是一个reflection矩阵,比如:

T04

reflection矩阵通常需要被特殊处理。比如,一个逆时针的三角形被reflection矩阵处理后,就成为了一个顺时针。这样可能会导致错误的光照和culling。为了检测一个矩阵是否为reflection矩阵,可以先计算出其行列式。如果行列式的值为负数,则矩阵为reflection矩阵。比如上述矩阵的行列式为0 * 0 - (-1) * (-1) = -1。

举个例子,假如想要沿着一个特定的方向缩放呢。标准的缩放矩阵只能沿着x、y、z轴进行缩放。如果想要沿着其他方向缩放,那么需要一个复合的transform。假设沿着正交坐标系fx,fy,fz进行缩放。那么,首先,先构建F的矩阵

T04

这样的话,先把坐标系转移到这个新的坐标系下,然后进行标准缩放,最后在把坐标系转移回来。所以,先乘以F的逆矩阵,然后乘以缩放,最后再乘以F将坐标系转回来。公式如下:X = FS(s)FT。

4.1.4 切变

另外一种transform叫做shearing矩阵。例如,这些可以用在游戏中用于扭曲整个场景以产生迷幻效果,或者扭曲模型的外观。有六个基本的shearing矩阵,分别为:Hxy(s)、Hxz(s)、Hyx(s)、Hyz(s)、Hzx(s)和Hzy(s)。第一个下标表示shearing矩阵改变的坐标,第二个下标表示shearing的坐标。shearing矩阵Hxz(s)的公式如下所示。需要注意的是,下标可用于确定参数s在矩阵中的位置,x表示第0行,z表示第2列,所以s位于:

T04

将该矩阵与点p相乘的效果将会新产生一个点:(Px+sPz,Py,Pz)T。下图为一个方块被shearing后的结果。Hij(s)的逆矩阵(i和j不同)为向相反方向shearing,也就是说H-1ij(s)= Hij(-s)

T04

上图为使用Hxz(s)对方形进行shearing。y和z值都没有发生变化,只有x值变成了原x+s*z,导致正方形倾斜。这种transform是不会改变面积的,比如图中的虚线部分其实是一样大的。

也可以用一些稍微不一样的shearing矩阵。比如在这里,两个下标都用来表示这些坐标将被第三个坐标shearing。关系为:H’ij(s,t) = Hik(s)Hjk(t),其中k是第三个坐标系的索引。根据各人的需要可以选择不同的shearing算法。最后,可以看到由于shearing矩阵H的行列始终为1,所以它是一个不会改变体积的transform,也正如上图所说的那样。

4.1.5 Transforms串联

由于矩阵的乘法中,矩阵的先后顺序很重要,所以transforms串联的时候,先后顺序也就很重要了。

如下图4.4所示,假如有2个矩阵,S(2,0.5,1)和R(ϖ/6)。S是将x坐标乘以2,y坐标乘以0.5。R是将沿着z轴(在右手坐标系中z轴为垂直于书指向外面的轴)逆时针旋转ϖ/6。这两个矩阵可以以两种不同的顺序进行串联,得到的结果也完全不同。

T04

串联矩阵的目的是为了提高效率。想象一下,在一个game视图中由数百万个顶点,每个顶点都需要被缩放、旋转、平移。与其将所有的顶点都去与这三个矩阵进行运算,不如将这三个矩阵串联成一个矩阵,然后将这个矩阵应用到顶点上。组合矩阵是C = TRS。注意这里的顺序。首先被应用到顶点上的是缩放矩阵S,所以S出现在组合的最右侧。所以:TRSp = (T(R(Sp))),其中p是要被transform的顶点。基本上,TRS的顺序是图形学领域最常见的顺序组合。

值得注意的是,虽然矩阵串联是顺序相关的,但是可以根据需要对矩阵进行分组。例如,使用TRSp,想一次性计算刚体运动的TR。可以将前两个矩阵合并,(TR)(Sp)。可见,矩阵串联满足结合律。

4.1.6 刚体Transform

当一个人抓住一个物件,比如将一支笔移动到另外一个地方,比如他的衬衫口袋中。这个过程笔这个物件只有位置和方向发生了变化,物件的形状并没有任何影响。这个transform,只是组合了平移和旋转,被称为刚体transform。它的特性是保持了物件的长度、角度。

刚体的矩阵X,是由平移矩阵T和旋转矩阵R组成,所以X大概是长下面这个样子

T04

X的逆矩阵为X-1 = (T(t)R)-1 = R-1T(t)-1 = (RT)T(-t)。也就是说,左上的3*3矩阵的R被转置,平移矩阵T改变符号。然后这两个新的矩阵按照相反的顺序相乘,就得到了逆矩阵。还有一个方式去计算X的逆矩阵,首先,我们先按照下面的符号表示X

T04

其中r,0为旋转矩阵的第一列,r0,T是矩阵的第一行,还有O是一个3*1的列向量(每个分量都是0)。然后逆矩阵的表达式为:

T04

示例:确定相机的方向。图形学领域一个常见的功能是将相机转向一个特定的位置。这里我们会使用gluLookAt(这是来自OpenGL Utility库,简称GLU)。尽管这个函数调用本身现在已经不怎么被使用了,但是这个任务依然很常见。假设摄像机位于C,希望摄像机观察目标L,给摄像机一个向上的方向u’,如图4.5所示。我们希望计算出一个三向量组成的基,(r,u,v)。首先我们先计算出来v = (c - L)/|| c - L||,v是物件到摄像机坐标的归一化向量。向右的向量也可以计算得到:r = -(V * U’)/||V * U’||。U’通常不能保证精确纸箱上,所以最终的向上向量还是需要通过叉积得到U = V * R,由于V和R都是归一化和垂直的,所以U也是归一化的。下面我们要构造相机的变换矩阵M,首先将相机的位置归零,然后将r对应(1,0,0),u对应(0,1,0),v对应(0,0,1),如下所示

T04 T04

相机位于C,向上向量为U’,看向点L。计算出R、U、V

注意,将平移矩阵和basis矩阵串联的时候,平移矩阵t放在最右侧,因为它最先被应用。有一个简单的方法去记住这个矩阵。因为我们希望r作为(1,0,0),所以将basis矩阵与(1,0,0)相乘的时候,得到的是r。同类可推u和v。

4.1.7 法线Transform

矩阵可以用于transform点、线、三角形以及其他几何体。同样的矩阵可以用于transform三角形的切线向量。但是矩阵不能用于转换一个重要的几何特性,面法线(以及顶点法线)。下图4.6展示了如果使用相同矩阵会发生什么

T04

上图左侧为原始的几何体,一个三角形和它的法线。中间展示了对法线实施x轴缩放0.5倍的矩阵后的结果。右侧图片展示了针对法线的正常transform。

所以不直接乘以矩阵,而使用矩阵的逆转置矩阵。由于法线的长度在transform后不一定为1,所以需要进行归一化。

使用逆转置矩阵的方法通常有效(参考文献[1794])。然而有时候也会失效,因为逆矩阵是用伴随矩阵除以原矩阵的行列式,但是如果行列式为0,矩阵为奇异的,就不存在逆矩阵。

即使只是计算4*4矩阵的伴随值也很昂贵,且通常不需要,因为法线是一个向量,所以平移不会对它产生影响。很多建模转换都是仿射的,通常不会改变齐次坐标的W分量。也就是不会执行投影。在这些情况下,只需要计算左上角3*3矩阵的伴随矩阵即可。

有时候甚至不用计算伴随矩阵。加入一个transform矩阵是由平移、旋转和统一缩放(无拉伸或挤压)操作组成。平移不影响向量。统一缩放至改变法线的长度,也不重要。只剩下旋转,而旋转为正交矩阵,逆矩阵即为转置矩阵,所以逆转置矩阵也就是矩阵本身。在这种情况下法线转换可以直接用原始矩阵操作。

另外,有时候不需要归一化。比如只有平移和旋转操作,那么法线的长度不会变化,也就不需要归一化。如果使用了统一缩放,则直接用整体缩放因子处理即可。比如我们知道一个物件被放大5.2倍,那么直接将法线除以5.2即可归一化。或者,建立一个可以产生归一化结果的法线矩阵,然后对原始矩阵进行一次性处理即可。

在transform之后,面法线依然垂直于三角形(比如通过三角形三条边进行叉乘获得),这个很正常。切线与法线不同,切线是直接通过原始矩阵计算获得。

4.1.8 逆运算

在许多情况下,需要对矩阵求逆,比如在坐标系之间来回变换。根据transform的信息,可以使用以下三种方式对矩阵求逆。

  • 如果矩阵是single transform或者一组已知参数的simple transforms的组合,那么可以通过颠倒矩阵顺序以及将参数取反的方式计算。比如:M = T(t) R(Φ),那么M-1 = R(-Φ)T(-t)。这样最简单且能保留transform的精度,在绘制大世界的时候非常重要(参考文献[1381])
  • 如果矩阵是正交矩阵,那么M-1 = MT,也就是说转置矩阵就是逆矩阵。无论多少个旋转矩阵组合在一起还是旋转矩阵,也就还是正交矩阵。
  • 如果什么都不知道,那么只能通过伴随矩阵、克莱默法则、LU分解或者高斯消元来计算逆矩阵。一般使用克莱默法则和伴随矩阵,因为它们的分支比较少。在现代体系结构中尽量避免if分支。在4.1.7中可以知道如何通过伴随矩阵得到法线transform的逆。

在优化的时候会考虑逆运算的目的。比如,如果逆运算是用来针对向量transform的,那么只会翻转左上角的3*3矩阵(参见前一节)。

4.2 特殊的矩阵和操作

本节我们会介绍几种对实时图形至关重要的矩阵变化和运算。首先,我们提出了欧拉变换(含参数),是用来以一个直观的方式描述方向。然后我们可以从一个矩阵获取到一组基本变换。最终得到一个沿任意轴旋转物体的方法。

4.2.1 欧拉变换

这种转换是一种直观的方式去构建一个矩阵,使得你或者相机或者其他物件朝向某个方向。它的名字来自伟大的瑞士数学家Leonhard Euler(1707-1783)

首先,必须建立某种默认的视图方向。最常见的是沿着负Z轴,头部朝向Y轴,如下图4.7所示。欧拉变换是三个矩阵的乘法,即如图所示的旋转。用公式来表达E如下方程式:

T04 T04

上述矩阵组合的顺序一共有24种。我们选择这个是因为它是最常被使用的。因为E是旋转的串联,所以它是正交的。因此它的逆矩阵可以表示为E-1=ET=(RzRxRy)T=RyTRxTRzT。当然也可以直接使用E的转置矩阵即可。

欧拉角h、p、r代表着旋转顺序以及它们各自沿着自己轴线的旋转读书。有时候这些角度也被称为roll,比如head->y-roll,pitch->x-roll,raw->z-roll。head也被称为yaw,这是飞行模拟中的说法。

这种transform很直观,也很容易被外行理解。比如,改变head的角度就是使观察者摇头,改变pitch的角度就是点头,roll就是倾斜头部。所以,我们不是在讨论x、y、z轴,额是讨论head、pitch、roll。请注意,此转换不仅可以确定相机方向,还可以确定任何物件的方向。这些转换可以使用世界空间的轴,也可以使用局部坐标系中的轴。

需要注意的是,一些欧拉角认为Z轴为初始方向。尽管可能会导致混淆,但实际上这个区别只是符号上的不同。在计算机图形学中,关于世界是如何形成的有一个分歧:z向上或者y向上。大部分制造领域,比如3D打印机,都认为z轴在世界空间中向上,航空、海上交通工具、建筑学以及GIS(地理信息系统)都认为Z轴向上,因为建筑平面图和地图是二维的,X和Y轴。而媒体相关的建模系统通常认为Y方向为世界坐标中的向上方向,这个与我们计算机图形学中相机的向上方向一致。这两个世界坐标系中方向的选择的区别仅仅是90度旋转(或反射),但是不确定一个就容易出现问题。在本书中,除非另有说明,我们将使用y向上的世界坐标系。

需要注意的是,摄像机在观察视角的向上方向与世界空间的向上方向不同。roll的时候,视角会倾斜,这个时候它的向上向量与世界坐标系的不同。另外举个例子,这个世界是y向上的,然后我们用相机去拍鸟瞰图的时候,摄像机需要沿着pitch旋转90度,这个时候它的向上方向为(0,0,-1)。这个时候在世界空间相机没有了y-component,-z成了向上方向。然而在view space,y向上依然成立。

虽然对小的角度改变或者观察方向很有用,但是欧拉角还有一些严重问题。比如很难组合使用两组欧拉角。两个欧拉角做插值并非简单的插值角度。实际上有可能2个不同的欧拉角实际上指定的是相同的方向,那么任何插值都不应该旋转物件。这也就是为什么引入了替代方案的原因(比如四元数),这些将在下面继续说明。而且使用欧拉角,还有可能得到万向节死锁的东西,这个将在4.2.2进行解释。

4.2.2 从欧拉角中提取参数

有时候,我们需要从正交矩阵中提取欧拉参数h、p、r。正交矩阵如下所示

T04

这里我们使用3*3的矩阵代替了4*4的矩阵,因为3*3的矩阵已经提供了旋转矩阵所需要的所有信息,而对应的4*4矩阵所多余的部分除了右下角为1,其它全为0

将上述方程中的三个旋转矩阵串联起来,得到:

T04

由此可见,pitch可以通过 sinP = e21获得。同时,将e01除以e11,e20除以e22,可以得到head和roll:

T04

这样的话,欧拉角h、p、r就通过矩阵E,以及atan2(y,x)(第一章第8页)获得:

T04

但是有一个特殊情况需要处理。如果cosP = 0,那么就会出现万向节死锁,roll和head将绕着同一个轴旋转(尽管可能是不同方向,取决于p旋转角度是ϖ/2还是-ϖ/2)(Patrick:其实万向节死锁的原理很简单,因为约定俗成旋转的轴线是先y->head/yaw,x->pitch,z->roll,首先旋转y,然后将x旋转90度后,z就和y轴在一条线上了。所以因为我们的旋转顺序是y/x/z,是万向节死锁只有在x轴的pitch90°会出现问题,head/yaw、roll90°都没问题)如果旋转顺序为x/y/z,那么y轴不能旋转90度,否则z轴的旋转就是多余的了。

当cosP=0的时候,就不能通过上述公式得到h和r了,因为e01、e11、e20、e22都是0,无法做除法。而我们假设h=0的话(参考文献[1769]),可得到下面矩阵:

T04

那么这样可以得到r=atan2(e10,e00)。

arcsin的定义为:-ϖ/2≤p≤ϖ/2,也就是说,如果p在这个区间之外,则可能存在无数值。所以H、P、R并非唯一的,也就是说可以通过多组欧拉角得到相同的转换。关于欧拉角更多的信息可以参阅shoemake 1994年的文章(参考文献[1636])。上述的简单方法可能会导致数值不稳定,但这是为了以防性能消耗过大。

从数学上,通过如下公式我们也能看到万向节死锁,比如假设cosp=0,即p = ±ϖ/2 + 2ϖk,其中k为整数。这样的话,我们就会失去一个轴向的自由度,因为矩阵只依赖一个角度:r+h或者r-h。

T04

虽然我们规定了欧拉角的顺序,但是也可以按照其他顺序。比如在动画中经常使用Z/X/Y,动画和物理也常用Z/X/Z(Patrick:Z/X/Z?确定不是打印错了?)Z/X/Z对于某些应用程序可能更优越,因为只有当X旋转90°时,才会出现万向节死锁。没有完美的顺序可以避免万向节死锁。尽管如此,欧拉角仍然是最常用的,因为动画师喜欢使用曲线编辑器来指定角度随时间变化的情况(参考文献[499])

示例:限制一个transform。假如你手里有一个扳手用于拧螺丝。为了拧紧螺丝,需要旋转扳手的X轴。现在你的输入设备(鼠标、手套、太空球等)为你提供一个旋转矩阵用于扳手的移动。但是这个旋转可能是错误的,因为我们只需要x轴旋转。要将旋转矩阵p限制为绕x轴旋转,只需要根据本届中提供的方法,提取欧拉角度h、p、r。然后创建新的矩阵Rx(p)即可。这个矩阵将绕X轴旋转扳手(当然前提是P中包含x轴的旋转)

4.2.3 矩阵分解

以上内容我们都是基于假设我们知道组成矩阵的原始transform。而一般不是这样的。比如,我们只知道物件旋转的串联矩阵,而不知道具体每个轴向的旋转矩阵。将这个串联矩阵分解为多个轴向上的transform,被称为矩阵分解。

    需要矩阵分解的情况有很多:
  • 仅提取对象的比例因子
  • 算出特定系统的转换(比如一些系统不允许使用任意的4*4矩阵)
  • 确定模型是否只进行了刚体变换
  • 在只有串联矩阵的情况下,对动画的关键帧进行插值
  • 从旋转矩阵中删除shearing

我们已经提出了两种分解,即得到刚体变换中的平移和旋转矩阵,详见4.1.6 刚体Transform,以及从正交矩阵推导出欧拉角,详见4.2.2 从欧拉角中提取参数

正如我们所看到的,剥离出平移矩阵很简单,只需要拿到4*4矩阵的最后一列即可。也可以通过检查矩阵的行列式是否为负来确定是否发生了反射(Patrick:反射是啥。。)。而要分离旋转、缩放、shearing就需要做更多的工作了。

还好,有一些关于这个课题的文章,并提供了代码。Thomas(参考文献[1769])和Goldman(参考文献[552、553])分别提出了针对不同类型transform的不同方法。Shoemake(参考文献[1635])改进了仿射矩阵的技术,因为他的算法独立于参照系,并试图分解矩阵以获得刚体变换。

4.2.4 绕任意轴旋转

如果有一个程序,能够使得绕着一个任意轴旋转某个角度,这样有时候会很方便。假设旋转轴r是归一化的,那么就应该创建一个transform绕着r轴旋转α角度。

为了完成上述工作,我们需要先旋转到一个空间,在这个空间,我们想旋转的轴为x轴。这是听过一个称为M的旋转矩阵完成的。然后执行实际的旋转,然后通过M-1(参考文献[314])进行转换。整个过程如图4.8所示。

T04

如上图所示。绕任意轴r旋转,是需要先找到一个标准正交基r、s、t,然后将这个正交基与标准基对齐,也就是r和x轴对齐。然后再对X轴旋转,最后再转换回来。

为了得到M,首先,我们需要找到两个垂直于R,且互相垂直的轴。重点是先找到第二个轴S,然后第三个轴t就可以通过第一个和第二个轴的叉积获取到。t = r * s。一种稳定的方式是,先找到r的三个成员中,绝对值最小的成员,将其设置为0,然后将剩下的2个成员中的其中一个取反。从数学上,表达式(参考文献[784])为:

T04

这样就保证了s和r是垂直的,而(r、s、t)就是正交的。Frisvad(参考文献[496])提供了一种不需要分支的方法,这样的话就会更快,但是精度会降低。Max(参考文献[1147])和Duff等人(参考文献[388])提高了Frisvad方法的精度。但无论是什么方法,r、s、t这三个矢量都用于创建旋转矩阵

T04

这个矩阵将向量R转换到了X轴,s转换到了y轴,t转换到了z轴。因此,绕任意轴r旋转α角度的矩阵为

T04

总结一下,首先,我们通过M将r旋转到x轴,然后我们通过Rx(α)沿着x轴旋转α度,然后我们通过M矩阵的逆矩阵旋转回来。因为M矩阵为正交矩阵,所以使用MT当做M的逆矩阵。

Goldman(参考文献[550])提出了另外一种绕任意归一化轴R旋转α角度的方法。这里我们简单的介绍一下他的转换公式

T04

在4.3.2节中,我们还将介绍另外一种方法,通过四元数。这节中的算法,针对一个向量旋转到另外一个向量会更有效率。

4.3 四元数

四元数是由Sir William Rowan Hamilton于1843年作为复数的扩展发明的,但直到1985年Shoemake(参考文献[1633])才将四元数引入计算机图形学领域(实际上Robinson(参考文献[1502])于1958年将四元数用于刚体模拟)。四元数似乎用来表示旋转和方向。它在很多方面都优于欧拉角和矩阵。任何三维方向都可以表示为围绕特定轴的单个旋转。有了特定的轴向和旋转,那么通过四元数transform to或者from都将很简单了,而使用欧拉角的话则不太好办。四元数还可以被用于插值,这个是欧拉角无法完成的。

复数有实部和虚部组成。每部分都是由两个实数组成,第二个实数乘以√-1。同样的,四元数也有四个部分。前三个值与旋转轴密切相关,旋转角度影响所有的四个部分(详见4.3.2节)。每个四元数由四个实数组成,每个实数与不同的部分关联。由于四元数有四个分量,我们将使用vector来表示它,但是为了区分,我们将其上面加了一个帽子^q。下面我们将先聊一下四元数的一些数学背景,之后再用它来构造各种有用的变换。

4.3.1 数学背景

下面我们先来聊下四元数的定义

定义。四元数可以用下面方式来定义,都是等价的。

T04

qw被称为四元数的实部,qv为虚部,而i、j、k为虚部单元。

对于虚部qv,我们可以使用所有的归一化向量运算,比如加、缩放、点积、叉积等。利用四元数的定义,两个四元数^q和^r的乘法运算,如下所示。需要注意,虚单位的乘法是非交换的。

T04

如上可见,我们使用了叉积和点积用于计算两个四元数的乘法。

然后,根据四元数的定义,我们还可以表示出加法、共轭复数、范数、identity

T04

范数很简单,虚部完全被干掉,只剩下实部部分。范数有时候也被表示为||^q||=n(^q)(参考文献[1105])。通过上述公式也可以推导出逆矩阵^q-1。使用公式^q-1^q = ^q^q-1 = 1来计算。

T04 T04

标量乘法满足结合律:s^q = (0,s)(qv,qw)=(sqv,sqw),^qs = (qv,qw)(0,s)=(sqv,sqw)。

T04

单位四元数,^q=(qv,qw),也就是n(^q)=1.也就是说^q可以通过下面公式表达

T04

其中||uq||=1,因为

T04

当且仅当uquq=1=||uq||*||uq||,单位四元数是最适合创建旋转和方向的方式,详见下一节。但是再次之前,需要做一些操作才能得到单位四元数。

复数的表达式为:cosΦ+i sin Φ= eiΦ四元数的表达式为

T04

单位四元数对应的log和power函数如下所示

T04

4.3.2 四元数transform

下面我们来研究四元数的一个子集,即单位长度的子类,单位四元数。单位四元数最重要的是它们可以表示任何三维旋转,并且这种表示非常紧凑和简单。

首先,我们聊一下为什么单位四元数可以表示旋转和方向。首先,先将一个点或者一个向量的四个分量 P = (Px,Py,Pz,Pw)组成一个四元数^P。然后假设我们有一个单位四元数^q = (sinΦUq,cosΦ)T04公式也将^p也就是点p绕着Uq轴旋转2Φ(Patrick:这一块有点复杂,基本没看明白)。由于^q是单位四元数,^q-1=^q*。如图4.9所示

T04

上图为用四元数表示的旋转变换的图示。^q=(sinΦUq,cosΦ)。绕着Uq轴旋转2Φ度。

^q的任何非零整数倍数得到的四元数的旋转与^q一样,也就是说^q和-^q所代表的旋转一样。也就是说,将轴Uq取反,以及将实数部分qw取反,会得到一个和原四元数旋转完全一样的四元数。也就是说,从一个矩阵获取到的四元数可以是^q或者-^q。

给定两个单位四元数,^q和^r,将它俩窗帘城一个四元数,作用于^p,公式如下:T04。这里^c = ^r^p,是一个串联了^r和^p的单位四元数。

矩阵转换

由于通常需要将几个不同的变换组合在一起,并且大部分变换都是矩阵形式。因此需要一种方法将公式4.43转换成矩阵。四元数^q可以通过如下公式转换成矩阵Mq(参考文献[1633,1634])。

T04

这里的scale s = 2/(n(^q) * n(^q))。也就是说,针对单位四元数,矩阵可以简化为

T04

四元数一旦构建成功,就不再需要三角函数,这样的话,在实际应用中就会提高性能。

而从正交矩阵Mq转到单位四元数的逆运算,就需要一些运算量了。我们从上述公式4.46中可以得到如下关系式

T04

如果我们知道了qw,那么四元数的另外三个成员组成的vq,也就能计算出来了,从而四元数^q也就得到了。qw是由矩阵Mq的迹(在线性代数中,一个n×n矩阵A的主对角线(从左上方至右下方的对角线)上各个元素的总和被称为矩阵A的迹(或迹数),一般记作tr(A)。)计算得到:

T04

如果四元数为单位四元数,那么结果就出来了

T04

但是有个问题,为了保持代码稳定性(参考文献[1634]),我们要避免被除数非常小的情况发生。那么假设t=qw*qw - qx*qx - qy*qy - qz*qz,这样的话,我们就得到如下关系式

T04

从以上关系式,我们就知道了qx、qy、qz、qw哪个最大。如果qw最大,则通过上面的公式就可以得到四元数。否则的话,我们可以就通过下面的关系式进行计算

T04

通过上述公式,就可以在避免除法运算的同时,得到了四元数。除此之外,Schuler(参考文献[1588])还提出了一种没有分支,但是需要使用四次平方根算法的计算方式。

球面线性插值

球面线性插值是:给出两个单位四元数^q和^r以及参数t∈[0,1],计算插值的一个操作。这个对于设置对象的动画很有用。对于插值相机方向没什么用,因为相机的向上向量在插值过程中可能会倾斜,这样一般会是一个错误的效果。

此运算的代数形式用复合四元数^s表示,如下所示

T04

然而,对于用软件实现,下面这个形式(slerp表示球面线性插值)更为合适

T04

上述公式中的Φ为:CosΦ=qxrx+qyry+qzrz+qwrw(参考文献[325])。对于参数t∈[0,1],slerp函数计算得到的插值四元数,这些四元数在一起,构建出来从^q(t=0)到^r(t=1)的四维单位球面上最短的弧(除非^q和^r刚好反向)。圆弧位于由^q,^r给出的平面与原点和四位单位球体的交点形成的圆上。如图4.10所示。计算出的旋转四元数以恒定速度绕固定轴旋转。这样一条速度恒定、加速度为0的曲线称为测地曲线(参考文献[229])。球面上的大圆是作为平面通过原点与球面的焦点形成,其中的一部分被称为大圆弧。

T04

单位四元数表示为单位球体上的点。函数slerp用于四元数之间的插值,插值路径是球面上的一个大圆弧。需要注意的是,从^q1到^q2的插值,与从^q1到^q3到^q2的插值是不同的,即使它们到达了相同的地方。

slerp函数非常适合对两个方向进行插值,固定轴、恒速。而使用欧拉角插值的话,就会出现问题。但是,实际上直接计算slerp需要用到三角函数,这样会比较耗性能。Malyshau(参考文献[1114])考虑到将插值四元数放到渲染管线中进行,不使用slerp,而直接在PS中使用归一化四元数,对于90度角,三角形方向的误差最大为4度。这个误差尚可接受。Li(参考文献[1039,1040])提供了一种更快的方式,且和slerp的精度一样。Eberly(参考文献[406])提供了一种快速计算slerp的技术,只使用到了加法和乘法。

当对更多的方向进行插值的时候,比如^q0,^q1,..^qn,我们想从^q0插值到^q1,然后到^q2,一直到^qn,可以直接用slerp来进行操作。比如先将^qi和^qi+1作为参数进行slerp,然后再用^q+1和^q+2作为参数进行slerp。但是这样会导致抖动,如图4.10所示。这个与线性插值点时的情况类似,参考720面的图17.3。可以在阅读了17章关于spline样条曲线的内容后再重新阅读下面一段内容。

针对这种情况,一种更好的插值方式是使用spline。我们将在^qi和^qi+1之间引入^ai和^ai+1。然后在^qi,^a,^a+1,^q+1之间进行球面插值,这些新生成的四元数的计算公式如下

T04

然后通过以下公式对四元数进行三次球面插值

T04

如上可见,squad函数似乎由多个球面插值slerp函数组成(17.1.1中是由多个点的线性插值函数组成)。插值会通过原始方向^qi,i∈[0,1,..,n],而不会通过^ai,因为^ai是用来指出原始方向的切线方向的。

从一个方向旋转到另外一个方向

一个常见的操作是通过尽可能短的路径从一个方向S转换到另一个方向T。四元数的数学大大简化了这一过程,并显示了四元数与此表示的密切关系。首先归一化s和t,然后计算单位旋转轴,称为u,计算出u=(s*t)//||s*t||。然后e=st=cos(2Φ),||s*t||=sin(2Φ),其中2Φ是s和t之间的夹角。表示从s到t的旋转四元数为^q=(sinΦu,cosΦ)。可以简化一下:^q=(sinΦ(s*t)/sin2Φ, cosΦ)。然后借用半角公式,可以简化为(参考文献[1197])

T04

以这种方式直接生成四元数,可以避免了当s和t同向的时候,s*t为0,并将其作为被除数的操作(参考文献[1197])。但是当s和t相反的时候,还是会出现被除数为0的情况。当出现这种情况的时候,其实任何垂直于S的旋转轴都可以旋转到T。

有时,我们需要一个从S到T的旋转矩阵。通过4.46的公式和一些转换,旋转矩阵为(参考文献[123])

T04

在这个方程中,我们使用了如下中间计算

T04

可以看出,所有的平方根和三角函数都优于简化消失了,因此这是创建矩阵的一种有效方法。注意,4.57和4.30的结构和功能相同,然而4.57并不需要三角函数

请注意,当s和t平行或者接近平时的时候必须小心,因为s*t=0。如果Φ为0,则可以返回单位矩阵。然而,如果Φ为π,则我们可以围绕任何旋转轴旋转π。改旋转轴可以通过对s和任何与s非平行的向量叉积获取(详见4.2.4)。Moller和Hughes使用了一种另一种方案,叫做Householder 的矩阵去处理这种特殊情况(参考文献[1233])

4.4 顶点混合

想象一下一个虚拟角色的手臂是由两部分组成,前臂和上臂,如图4.11所示。该模型可以使用刚体变换(详见4.1.6节)进行动画制作。但是,这两个部分之间的连接不会像一个真正的肘部一样,因为它们是两个独立的对象,因此关节是由两个独立对象的重叠部分组成。显然,最好是将手臂看成一个对象,但是这样的话,又无法解决关节活动的问题。

顶点混合是解决这个问题的一个常见方法(参考文献[1037、1903])。这种技术还有其他几个名字,比如线性混合蒙皮、包络enveloping或者骨架子空间变形。虽然这里给出的算法的确切起源还不清楚,但是定义骨骼和皮肤对其变化做出反应是计算机动画中的一个古老概念(参考文献[1100])。最简单的形式是,前臂和上臂像以前一样分开动画,但是在关节处,两个部分通过一个弹簧皮肤链接起来。这样的话,这个连接处的部位将会有一部分顶点是由前臂矩阵控制,另外一部分顶点是由上臂矩阵控制。这样的话,三角形的顶点就可能会由不同矩阵转换,而不是每个三角形都只使用一个矩阵,见图4.11。

T04

上左图为由前臂和上臂组成的手臂,将它们拆成两个独立物件进行刚体变换动画。肘部看起来不真实。在右侧,将顶点混合作用于一个对象上。最右边的手臂说明了当一个简单的皮肤直接连接两个部分来覆盖肘部会发生什么,会发生顶点混合,一些顶点使用不同的权重混合(2/3,1/3)意味着顶点将上臂矩阵的权重变成2/3,前臂矩阵的权重变成1/3。在最右边的图中,还显示了顶点混合的缺点。可以看到肘部内部的折叠,如果使用更多骨骼和更好的权重可能会得到更好的结果。

更进一步的说,可以使得一个顶点受到多个不同矩阵的影响,将得到的位置加权混合在一起。这是通过为动画对象提供骨骼骨架来完成的,每个骨骼的变化都会通过用户定义的权重影响每个顶点。由于整个手臂可能具有弹性,每个顶点都可能受到不止一个矩阵的影响,所以整个mesh通常被称为皮肤。如图4.12所示。许多商业建模系统都有了骨骼建模功能。虽然被称为骨骼,但是也并不一定必须是刚体。Mohr和Gleicher(参考文献[1230])提出了增加更多的关节以实现肌肉膨胀等效果。James和Twigg(参考文献[813])提出了可以将骨骼挤压和拉伸后进行动画蒙皮。

T04

顶点混合的真实例子。左上图显示了一只手臂的两块骨骼,处于伸展位置。在右上角,将显示网格,颜色代表每个顶点对应的骨骼。下图为渲染后的手臂(图片由Jeff Lander(参考文献[968])提供)

从数学上讲,可以通过4.59公式表示,其中P为原始顶点,U(t)为转换后的顶点,其位置取决于时间t

T04

有n根骨骼影响P的位置,用世界坐标表示。Wi为顶点p所占骨骼i的权重。矩阵Mi将从初始骨骼的坐标系转成世界坐标。通常骨骼的控制关节位于其坐标系的原点。例如,前臂骨骼会降肘关节移动到原点,动画旋转矩阵会围绕关节移动手臂。Bi(t)是第i根骨骼的世界变换,随着时间的变化而改变对象的动画,通常是几个矩阵的串联,比如上面几级骨骼的变换矩阵以及本地动画矩阵。

Woodland(参考文献[1903])深入讨论了一种维护和更新Bi(t)矩阵动画函数的方法。每个骨骼将一个顶点genuine矩阵进行计算,然后将顶点进行插值得到最终结果。矩阵Mi在蒙皮的时候没有被详细说明,而是经常被认为是Bi(t)的一部分。我们在这里介绍它,是因为它很重要,几乎总是矩阵串联过程中的一部分。

总的来说,矩阵Bi(t)和Mi-1是针对动画的每一帧的每个骨骼,每个生成的矩阵被用于转换顶点。顶点p是由不同的骨骼连接矩阵转换,然后通过权重Wi混合,这就是顶点混合。权重为非负,且和为1,所以将顶点转换到几个位置后,再进行插值,而最终生成的顶点必然是这些中间顶点所形成的凸包内。也可以使用公式4.59转换法线,但是如果骨骼被拉伸或者挤压的比较严重,则可能需要Bi(t)和Mi-1的逆转置矩阵,如4.1.7节所述。

顶点混合非常适合GPU。网格中的一组顶点可以放置在一个静态缓冲区中,然后一次性发给GPU并重复使用。在每一帧中,只有骨骼矩阵发生变换,VS才会重新计算Mesh(Patrick:方法很多,CS或者TFBO)。通常这种方式,CPU和GPU的压力都会编导最小。如果一个人只有一个skinmesh,那么最简单,否则的话,多个skinmesh还需要计算多次骨骼(Patrick:其实也不需要吧,骨骼离线算好就好了)。另外骨骼变换可以存储在纹理中,以避免达到寄存器存储限制。通过使用四元数表示旋转,这样的话每个变换就可以存在两个纹素中(参考文献[1639])。如果支持的话,UAV也可以存储skin的结果(参考文献[146])

也可以指定超过[0,1]范围的值,或者和不为1的权重值。但是,只有在使用其他混合算法(比如4.5节的morph变形算法)的时候,才有意义。

顶点混合的缺陷在于发生了不必要的折叠、扭曲和自相交(参考文献[1037])。见图4.13。更好的解决方案是使用双四元数(参考文献[872、873])。这种蒙皮技术保持了原始transform的刚体性,避免了肢体扭曲。计算量小于线性蒙皮混合的1.5倍,且结果很好,以至于其得到快速应用。然而双四元数蒙皮可能会导致膨胀效应,Le和Hodgins(参考文献[1001])提出了一个更好的而选择:中心旋转蒙皮。假设局部变换为刚体,具有相似权重Wi的顶点,具有相似的transform。对每个顶点预先计算旋转中心,同时施加正交约束以防止肘部塌陷和candy wrapper twist效果。在运算时,该算法类似于线性蒙皮混合,GPU先在旋转中心执行线性蒙皮混合,然后再执行四元数混合。

T04

左上图可以看出使用线性蒙皮混合的时候关节处的问题。右上图中,双四元数混合解决了这个问题(图片由Ladislav Kavan等提供,模型由Paul Steed提供(参考文献[1693]))

4.5 变形

当做动作的时候,如果可以从一个3D模型变形到另外一个3D模型,那么效果会非常好(参考文献[28、883、1000、1005])。想象一下,在t0时刻显示成一个模型,在t1时刻显示成另外一个模型。在t0到t1的过程中,持续的发生类似插值的变换。图4.14显示的就是变形的过程。

T04

顶点变形。为每个顶点定义两个位置和法线。在每一帧中,通过VS插值得到中间的位置和法线(图片由NV提供)

变形涉及到两个主要问题,即顶点对应问题和插值问题。对于两个任意模型,它们可能具有不同的拓扑结构、顶点数量和mesh连接,通常需要先建立这些顶点的相应关系。这是一个复杂的问题,在这个领域有很多研究。可以参照Alexa的调查(参考文献[28])

如果两个模型之间已经存在一一对应关系,那么可以直接基于每个顶点进行插值。也就是说,对于第一个模型的每个顶点,在第二个模型中都只能存在一个顶点,反之亦然。这使得插值变得很简单。例如,可以针对顶点使用线性插值(也可以使用17.1中提到的其他插值方式)。为了计算t∈[t0,t1],我们首先计算s=(t-t0)/(t1-t0),然后计算线性顶点混合,m = (1 - s)P0 + sP1。其中P0和P1是同一个顶点在t0和t1时间的值。

变形中的一个比较常见的用法是morph targets或者blend shape(参考文献[907])。图4.15可以展示其基本原理

T04

图中有2个口型的pose,通过它们可以得到一组用于插值的向量,甚至更严重的变形。将向量作用于原始脸部,正权重的向量,可以给我们带来一个微笑的嘴,负权重则会产生相反的效果。

我们从一个原始模型开始,在这个例子中是一张脸,用N来表示这个模型。另外,我们还有一组不同的面部pose。在这个例子中,只有一个姿势笑脸。一般,可以有K≥1个不同的pose,被称为Pi,i∈[1, k]。然后先进行预处理,Di = Pi - N。也就是从每个Pose中减掉原始pose。

这样的话,我们就有了一个原始模型N,以及一套不同pose Di。然后一个新的变形模型M就可以通过下面公式计算出来了

T04

这样的话,就可以根据原始模型,以及不同的pose以及其权重Wi得到不同的效果,上述例子中,是通过W1 = 1得到微笑的脸(Patrick:这里和前面的插值不一样了,w1=1得到的是中间插值,而非t1时的模型)。使用W1=0.5可以得到一个半微笑的脸,以此类推。权重还可以为负数或者大于1的数。

对于这个简单的面部模型,我们可以添加另外一个有“悲伤”眉毛的面部。对眉毛使用负权重可以创建“快乐”的眉毛。由于位移是附加的,所以眉毛的姿势可以与微笑的嘴的姿势一起使用。

morph targets(blend shape)是一个非常强大的技术,可以给动画师提供更多的控制力,且这些还可以独立于其他动作进行操作。Lewis等人(参考文献[1037])引入了姿势空间变形,接合了顶点混合和morph targets。Senior(参考文献[1608])使用预计算的顶点贴图去存储和检索目标姿势之间的位移。支持流输出的硬件,以及通过每个顶点的ID,允许使得在单个模型总使用更多的目标,且仅在GPU计算(参考文献[841、1074])。可以使用一个低精度的mesh,然后通过TS生成高精度的mesh,通过displacemenet mapping避免在高精度模型中对每个顶点进行蒙皮的工作(参考文献[1971])

图4.16展示了一个同时使用蒙皮和变形的例子。Weronko和Andreason(参考文献[1872])使用蒙皮和变形在The Order:1886

在inFAMOUS Second Son中,角色Delsin的脸部使用了blend shape。所有这些镜头都使用相同的原始脸部姿势,然后通过不同的权重使得脸部看起来不同。(图片由顽皮狗公司提供)

4.6 几何缓存播放

在过场动画中,可能会需要精度特别高的动画。比如,一些无法用上述方法展现的动作。一个简单的方法是保存下来每帧中所有的顶点数据,将它们保存在硬盘里,然后用于更新mesh。但是这样的话,一个30000面的模型就需要50M/S的数据存储量。Gneiting(参考文献[545])提供了几种将内存降低到10%左右的方法。

在这里使用到了Quantization。先把位置坐标和纹理坐标使用16位的整数存储,这个步骤是有损的,因为在执行压缩之后,不能恢复原始数据。然后,为了进一步减少数据,进行了空间和时间预测,并对差异进行编码。对于空间压缩,可以使用平行四边形预测(参考文献[800])。对于triangle strip,下一个顶点的预测位置是当前三角形在三角形平面内绕当前三角形边缘绕成的一个平行四边形。然后,对这个新的位置与实际位置的差异进行编码。有了良好的预测,大多数差异为0,这是许多常用压缩方案的理想状态。与MPEG压缩类似,预测也在时间维度中进行。也就是说,每N帧执行一次压缩。例如,某个顶点根据delta向量从第一帧移动到了第n帧,那么它在第n+1帧可能会以相似的幅度移动。这些技术有效的减少了存储量,使得系统能够用于实时数据流传输。

4.7 投影

在实际渲染场景之前,场景中所有的对象都必须投影到某种平面或者某种简单体积上。之后,执行裁剪和渲染(详见2.3节)。

到目前为止,本章中的所有转换都没有处理第四分量,w分量。也就是说,点和向量在转换后保留了它们的类型。而且,4*4矩阵最下面的一行总是(0,0,0,1)。不过,透视投影矩阵不是这样的:最下面一行包含向量和点操作数,并且通常需要均匀化过程。也就是说,w通常不为1,所以需要除以W才能得到非齐次点。正交投影比较简单,也是常用的投影方法,在本节中首先讨论,它不影响w分量。

在本节中,假设观察者沿着相机的负Z轴观看,Y轴指向上方,X轴指向右侧。这是一个右手坐标系。有些文本和软件,例如DX,使用左手坐标系,观察者将沿着相机的正Z轴观看。这两个系统同样有效,最终将达到同样的效果。

4.7.1 正交投影

正交投影的一个特点是平行线在投影后将保持平行。当使用正交投影查看场景时,无论到相机的距离如何,对象都将保持相同的大小。矩阵P0(如下所示)是一个简单的正交投影矩阵,使得点X和Y分量保持不变,同时将Z分量设置为0。即,它以正交方式投影到平面Z=0上。

T04

投影的效果如图4.17所示。显然P0将不可逆,因为它的行列式|P0|=0。换句话说,将把物体从三维下降到两维。使用这种正交投影有问题就是,它将带正的点和带负的点都投影到了投影平面上。通常,需要将Z值(以及X和Y值)限制在一定的范围内,也就是n(近平面或者front plane或者hither)和f(远平面或者back plane或者yon)。这是下一个转换的目的。

T04

上图为正交投影产生的三种视图。这种投影可以被认为是观察者沿着负Z轴观察,也就是说保持X和Y坐标的同时,将Z坐标设置为0。Z=0平面两侧的物件都被投影到了平面上。

执行正交投影的更常见的矩阵为(l、r、b、t、n、f),表示左右上下近远六个平面。该矩阵将这些平面形成的轴对齐边界框(AABB,见22.2)转换为围绕原点居中的轴对齐立方体。AABB最小的点为(l、b、n),最大点为(r、t、f)。需要知道n>f,因为我们是在这个空间中向负Z轴看。但是我们的尝试是,近值应该比远值小,所以可以让用户来提供,再在内部取反。

在OpenGL中,轴对齐的立方体最小点为(-1,-1,-1),最大点为(1, 1, 1)。在DX中,边界为(-1,-1,0)和(1,1,1)。这个立方体被称为规范化视图体,其中坐标被称为NDC(normalized device coordinates)。转换过程如图4.18所示,转换到规范化视图体的目的是为了裁剪更高效一些。

T04

上图演示了转换的过程。左上图为第一步,先将重点移动到原点位置,然后进行缩放,缩放到规范化视图体的大小。

完成了到规范视图体的转换后,下一步将是根据这个cube做裁剪。cube之内被裁剪后的几何体将会被映射到屏幕上渲染出来。正交投影矩阵如下所示:

T04

正如这个方程所示,P0可以写成是一个转换的串联。先是T(t),然后是S(s)。其中s = (2/(r-l), 2/(t-b), 2/(f-n)),t=(-(r+l)/2, -(t+b)/2,-(f+n)/2)。矩阵是可逆的(当l、r,b、t,n、f不同时)。Po-1=T(-t)S((r-l)/2, (t-b)/2, (f-n)/2)。

在计算机图形学中,在投影矩阵后常会变成左手坐标系,用于viewport,x向右,y向上,z向viewport。由于我们定义的AABB中far值小于near值,正交投影通常会带一个mirror transform。然后AABB的坐标的左下角(l、b、n)将会变成(-1,-1,1),右下角(r、t、f)将会变成(1,1,-1)。将下列公式与公式4.63一起使用

T04

这是个mirror transform将右手坐标系转成了左手坐标系。

DX的Z值为[0,1],OpenGL的为[-1,1]。可以在正交投影后通过一个矩阵来处理这个事情

T04

所以DX中的正交矩阵为

T04

DX通常使用行主形式来编写矩阵,所以通常会以转置矩阵的形式出现。

4.7.2 透视投影

比正交投影更复杂的转换是透视投影,这在大多数计算机图形应用中都是常见的。在这里,平行线在投影后通常不平行,相反,它们可能在其末端收敛到一个点。透视投影更接近我们对世界的感知,即更远的物件更小。

首先,我们将给出投影到平面Z=-d,d>0的透视投影矩阵的指导性推导。我们从世界空间触发,简化了对整个过程的理解。针对这种推导,OpenGL(参考文献[885])中有更方便的矩阵。

假设相机位于原点,我们想要将点P投影到Z=-d的平面上,d>0,从而产生一个新的点q=(qx,qy,-d)。整个过程如图4.19所示。从图中的三角形可以看出来,qx为:

T04 T04

上图是用于推导投影矩阵。点P被投影到Z=-d,d>0的平面上,产生的点q。投影是从相机位置角度执行的,在本例中,是从原点开始。推导过程中使用的相似三角形为右侧的X分量。

相应的qy=-d*py/pz,qz=-d。综上所述,我们得到投影矩阵Pp为:

T04

下面公式可见,根据透视矩阵得到了正确的透视投影

T04

其中最后一步是将整个vector除以w分量,然后最后一位为1。而z值始终为-d,因为我们投影的目标就是-d。

其实很容易能理解为什么要除以w,因为其几何解释是,它将点(Px、Py、Pw)投影到平面w=1上。

和正交投影类似,透视投影也会将视图转换到规范化视图体内。透视投影的视图从z=n出发,到z=f结束。从z=n平面可以得到最小点(l、b、n),而在n=f矩阵可以得到最大点(r、t、n)。如图4.20所示。

T04

矩阵Pp将视图转换成单位cube,被称为规范视图体。

参数(l、r、t、b、n、f)决定了相机的视椎体。水平区域是由截锥的左右平面决定。垂直区域是由截锥的底面和顶面决定。FOV越大,相机看到的越多。当r≠-l或者t≠-b的时候,会出现不对称感觉,可用于立体观看或者VR(详见21.2.3)(Patrick:这个有意思。。)

FOV是提供场景感的一个重要因素,与计算机屏幕相比,眼睛本身有一个物理FOV。关于为:Φ=2arctan(w/(2d))。其中,Φ为FOV,w为垂直于视线的物体的宽度,d为到物体的距离。比如,一个25英寸的显示器大约是22英寸宽。在12英寸外,水平FOV为85度,20英寸处,为58度。30英寸处,为40度。同样的公式也可用于将相机镜头尺寸转换为FOV。比如35mm的相机(具有36mm宽的帧尺寸),对应标准50mm的镜头时,FOV Φ=2arctan(36/(2*50))=39.6度。

使用一个比物理FOV(Patrick:35mm镜头对应的就是物理FOV)更窄的视野将会降低透视效果,因为玩家可能会对镜头进行Zoomed in。而设置更宽的FOV会使得对象看起来扭曲(比如使用广角摄像机镜头),尤其在屏幕边缘附近,并且会放大附近对象的比例。但是,更广阔的视野让观察感觉到物体更大,更令人印象深刻,并且有利于用户提供更多有关周围环境的信息。

下面是将截锥转成单位立方体所使用的透视转换矩阵。

T04

当使用上述矩阵对点做了转换后,就可以得到一个新的点q=(qx,qy,qz,qw)T。其中w分量qw,一般不为0或者1。然后为了得到真正的投影点p,我们还需要对其进行除以qw的操作。如下:p=(qx/qw,qy/qw,qz/qw,1)。然后,就可以看到z=f的点会映射到+1,z=n的点会映射到-1。

远平面之外的物件会被裁剪掉,将不显示在场景中。投影矩阵可以将一个远平面当做无穷远,使用的矩阵为

T04

综上所述,投影矩阵Pp之后,会进行裁剪和归一化(除以w),以得到NDC(normalized device coordinates)。

为了得到OpenGL中使用的透视变换。首先,先乘以S(1,1,-1,1),原因同正交变换相同。这样就会将上面公式4.71中的第三列的值取反。经过这次镜像后,near和far的值将变成负数,也就是0 T04

还有一种更简单的方法,就是通过水平FOVΦ来表示,也就是宽高比a = w/h,w和h为屏幕分辨率,n'和f'。公式为

T04

其中c=1.0/tan(Φ/2)。这个矩阵实现了老的API gluPerspective()公式的工作,该API来自OpenGL Utility Library(GLU)。

一些API(比如DX),会将近裁剪面映射到z=0(OpenGL为-1),远裁剪面映射到z=1。而且DX使用的是左手坐标系定义投影矩阵。也就是说DX是看向Z的正轴,将near和far作为正数。下面是DX所使用的公式

T04

DX的文档一般使用row-major矩阵,所以上述矩阵一般会以其转置矩阵的方式展现。

使用透视矩阵会导致一个事情,会导致计算得到的深度值,与输入的Z值非线性关系。通过上述几个公式与点p相乘,会得到以下结果

T04

其中Vx和Vy被忽略,常数d和e取决于所使用的矩阵。加入我们使用的是4.74公式,则d=-(f'+n')/(f'-n'),e=-2f'n'/(f'-n'),vx=-pz。下面为了得到归一化设备坐标中的深度,我们需要除以w分量,得到如下结果

T04

其中OpenGL中的Zndc∈[-1,1]。我们可以看到输出的Zndc与深入深度Pz成反比。

举个例子,在OpenGL中,假如n'=-10,f'=-110,当Pz沿着Z的负轴向下60个单位(中点的位置)。其NDC坐标为0.833,而非0。图4.21显示了改变近平面和原点之间距离导致的效果。其实近远平面都会影响Zbuffer的精度。这个将在23.7讨论。

T04

上图为改变近平面与原点之间距离的效果。保持f’与n’之间的距离为100,。随着近平面越来越靠近原点,远平面附近的点能使用的NDC中的范围越小。这会使得ZBuffer在远距离会比较不精确。

有几种方法可以提高深度精度。一种比较常见的方法,叫做reversed-z,是用浮点数或者整数存储1.0-Zndc(参考文献[978])。下图4.22可以看到对比。Reed(参考文献[1472])通过模拟表明,使用反向Z的浮点缓冲区可以提供最佳精度,即使与包含24位深度的整数深度缓冲区对比。对于标准映射(非反转Z),Upchurch和Desbrun(参考文献[1803])建议分离转换中的投影矩阵可以降低错误率。例如,使用P(Mp)比T(P)要好,其中T=PM。另外,在[0.5,1.0]的范围内,fp32和int24的精度类似,因为fp32有一个23位的位数。将Zndc映射到1/pz的原因是,这样会使得硬件更简单,深度压缩的更好,这个将在23.7节详细讨论。

T04

上图展示了DX中多种不同的创建深度缓冲区的办法,Zndc∈[-1,1]。左上图,标准整数深度缓冲区,只有4位精度(可以看到y轴上的16个标记)。右上:远平面设置为正无穷大,与左上图对比可以看到,这样的话精度损失并不大。左下图,使用3个指数位和3个尾数位的浮点深度缓冲区。可以看到Y轴上的分布为非线性的,使得X轴上的分布更糟糕。右下图,反向浮点深度,1-Zndc,这样分布很好(图片由Nathan Reed提供)

Lloyd(参考文献[1063])建议使用深度值的对数来提高阴影贴图的精度。Lauritzen等人(参考文献[991])使用前一帧的Zbuffer确定近裁剪面的最大值和远裁剪面的最小值。对于屏幕空间深度。Kemen(参考文献[881])建议使用以下公式对每个顶点进行重新映射

T04

其中w为透视投影矩阵后的w,z是vs的输出。fc=2/log2(f+1),其中f为远平面。当使用了改变换后,深度将变为线性插值。由于log是单调函数,所以OC、深度压缩等技术依然可以使用。当曲面细分做的足够细的时候没问题,或者也可以直接在PS中进行这个操作。在VS中逐顶点的计算e=1+w,然后经过插值虎,在PS中逐像素的计算depth=log2(ei)fc/2,其中ei是e经过插值后的结果。当GPU不支持浮点深度时,且需要很大范围深度的时候,可以使用这个办法

Cozzi(参考文献[1605])建议使用多个Frusta,这样可以有效的提高精度,达到任何期望的精度。先将视椎体在深度方向上分成几个不重叠的小的截锥,合在一起刚好是视椎体。子frusta按从后到前的顺序呈现。然后,清理颜色缓冲区和深度缓冲区,并将渲染的所有对象排列到它们对应的子frusta中。对于每个子frusta,设置其投影矩阵,清理深度缓冲区,然后渲染。

更多资源

http://immersivemath.com 网站是一个关于本章主题中基础知识的可交互的网站,可以通过操作的方式建立对这些基础知识的感性认识。其他交互式学习工具或者相关代码可以从www.realtimerendering.com获取到。

Farin和Hansford的《The Geometry Toobox》(参考文献[461])是一本帮助建立关于矩阵感性认识的书,除此之外,还有Lengyel的《Mathematics for 3D Game Programming and Computer Graphis》(参考文献[1025])。其它的,比如Hearn和Baker(参考文献[689]),Marschner和Shirley(参考文献[1129])以及Hughes等人(参考文献[785])所著作的许多计算机图形文本,也都包含了很多矩阵相关的知识。Ochiai等人(参考文献[1310])极少了额矩阵的基础以及矩阵的指数、对数等知识,用于计算机图形学。《Graphics Gems》系列(参考文献[72、540、695、902、1344])提供了各种与转换相关的算法,并在网上有很多这样的代码。Golub和Van Loan的矩阵计算(参考文献[556])是一本非常适合入门的矩阵技术书籍。Lewis等人在SIGGRAPH上的论文(参考文献[1037]),很好的介绍了skeleton-subspace变形和顶点混合和形状插值等信息。

Hart等人(参考文献[674])和Hanson(参考文献[663])提供了四元数的可视化。Pletinckx(参考文献[1421])和Schlag(参考文献[1566])提供了在一组四元数之间平滑插值的不同方法。Vlachos和Isidoro(参考文献[1820])推导了四元数的C2插值。四元数插值还关联着另外一个问题,就是沿着曲线计算一致坐标系。Dougan(参考文献[374])对此有研究。

Alexa(参考文献[28])、Lazarus、Verroust(参考文献[1000])对于很多不同的变形技术进行了研究。Parent的书(参考文献[1354])是一本关于计算机动画技术的很好的书籍


虽然并非全部原创,但还是希望转载请注明出处:电子设备中的画家|王烁 于 2019 年 5 月 27 日发表,原文链接(http://geekfaner.com/shineengine/Translation4_RealTime_Rendering_4th_Edition1.html)