目 录
  2.1 欧几里得空间,自由度和基本变换
  2.2 平移
  2.3 缩放
  2.4 在平面内旋转
  2.5 3D旋转
   2.5.1 坐标系
   2.5.2 变换次序
  2.6 以矩阵形式表达变换
  2.7 投影变换
   2.7.1 平行投影
   2.7.2 透视投影
  2.8 通过定点算法实现变换
   2.8.1 整型数表示
   2.8.2 定点数运算
   2.8.3 定点算法的实现



  引言
  真实的现实由物质组成。物质可以反射光线,使它们本身可以被人看到。在计算机图形意义上,术语中的虚拟现实并不作为物质而存在。它只是在可视化算法和计算机硬件帮助下的某种抽象实体的解析描述,当人们看上去的时候类似物质和现实。
  因此,在计算机图形学上的第一个挑战就是找到一种解析地描述对象的途径,然后是找到一种数学设备来支持可视化算法。几何学和线性代数的方法都在计算机图形学中使用,典型地,我们使用几何学和线性代数来解决问题。在这一章我们将要主要集中地讨论有关不同坐标变换的数学方法。变换是把一个坐标系中的点映射到另一个坐标系的方法。对象在虚拟世界中的运动和投影到屏幕上要使用不同的坐标变换来计算。为了有效地计算,我们也考虑一些实现变换以及计算方法的技术问题。

2.1、欧几里得空间,自由度和基本变换
  在几何学中,物质的形状在根本上基于空间中一些点的坐标。我们在一个局部范围感知到的周围的世界,在数学上对应于欧几里得空间。
  平面上一个单独的点,在欧几里得空间中,可以明确地用笛卡尔坐标指定:(x,y)。(参见图2.1)

图2.1 笛卡尔坐标系的2D平面


  同样地,我们周围的世界(包含了三个维),也能够被欧几里得三维空间模拟。在这个空间中一个单独的点被三个坐标指定:(x,y,z)
  在随后的几章中,我们还要考虑表示虚拟世界的许多方法。这些方法本质上提供了如何确定所描述的对象上点的(x,y,z)坐标的方法。一些表达方法也许明确地指出对象的关键点,比如说多面体上的顶点。
  为了表达虚拟世界的一些参数,我们可以使用简单的标量值。一个实数或一个整数就是一个标量。举例来说,标量可以被用于表达虚拟场景中所有方向上的恒定的照明度。其它的情况除了处理数量级外,也处理方向。传统的物理学例子是强制应用的材质点。这些实体清楚地包含了方向和数量。同样,在虚拟场景中有方向的光也要处理方向和数量。为了表达这些,我们使用向量。一个向量可以被看作有方向的线段,向量的数量等于线段的长度。很显然,有无穷的等方向、等长度的平行线段可以用同一个向量来表示。但是,在这些线段中,只有一个的起点在坐标系的原点。因此,我们可以用一个这样的线段唯一地描述一个向量。在平面上的向量表示为,坐标被用于指定向量线段的终点,这个向量被假定起始于坐标系的原点。(参见图2.2)

图2.2 决定向量坐标


  图2.2也指出, 当我们给定一些有向线段QP,我们可以通过移动线段的原点到坐标系的起点来推出这个线段表示的向量坐标。换句话说,向量坐标可以通过从线段终点P的坐标中减去线段起点Q的坐标计算出来:


  同定义标量坐标的运算一样,比如说实数或整数进行加或减,也可以定义向量运算集。最重要的两个就是通过标量来定义的向量加法和向量乘法。两个向量的和定义为:坐标是两个给定向量坐标之和的向量:


  向量可以被标量乘,得到另一个向量,其坐标值通过用给定的标量去乘给定的向量的坐标得到:


  向量加法可以表达几个向量实体的组合效果。乘以标量能改变向量实体的数量和方向,而方位不变。举例来说,用标量-2去乘一个向量,改变了向量的方向,长度也增加了一倍。图2.3展示了这些操作的几何意义。

图2.3 向量运算(乘、和、差)


  其它关于向量的运算也可以同样地定义。举例来说,向量减法可以被定义为:与乘以标量-1的向量的和:(参看图2.3)


  在后面的几章,我们将要遇到向量的两个其它操作:标量积和向量积(即点乘和叉乘)。
  需要指出的是,标量和向量使我们能够创建一个虚拟世界的描述,对象在这个世界中移动和改变方向。如果约束对象体,使所有的点之间的距离不变,则这种刚体在平面上占有3个自由度,在3D空间占有6个自由度。每个自由度都是一个标量参数,它的改变引起了前面所说的系统状态的改变。举例来说,线段上的一个点在任何状态下只有一个自由度(点的位置),它可以通过一个单独的参数来指定,即线性坐标。对2D空间中的刚体来说,自由度沿着两个坐标轴位移(平移)以及沿着原点旋转。刚体从某个原始位置到任何其它位置的变换可以通过这3个参数变量表示出来。在3D空间中,有三种不同的独立的位移可能。每个都沿着三个坐标轴中的某一个移动以及围绕三个不同的坐标轴旋转,这三个坐标轴都与形成坐标系的坐标轴相平行。
  总的来说,刚体的任何变换都可以表示为两种不同的变换类型:平移和旋转。对大多数计算机图形应用来说,合成的对象要保持形状和顶点坐标不变。在这一类应用中,我们最需要的就是刚体变换。然而,如果对象的形状(点之间的距离)变化了,我们同样要考虑其它的变换。可能最普遍的非刚体变换就是缩放,缩放导致了变换后的对象在大小和形状上的改变。
  谈到所有的3D变换,我们要么把它们作为在静态坐标系中的点集来考虑,要么就作为空间中的变换,换句话说,改变的是坐标系。我们将要看到,对一些坐标变换来说首先要考虑的是方便性,但对另一些则是其次要考虑的。

2.2、平移

  平移变换,正如我们已经简要地讨论过的,是把对象上的点移到一个新的指定的位置(参看图2.4 (a))。我们也许也要考虑随着坐标系(空间)的移动导致的平移变换。(参图2.4 (b))

图2.4 点集平移和空间平移


  正如图2.4展示的那样,如果我们移动点集,在这些点之间的距离不会改变。任何在2D空间中的移动都被分割为两个独立的分量:沿着X轴的移动和沿着Y轴的移动。平移变换需要的最重要的一点是在虚拟屏幕坐标系上开始移动的起始点(或者说是原点)对应于物理屏幕的中心,物理屏幕通常有一个参考系,原点位于屏幕左上角。程序清单2.1展示了可能的平移例程实现:

程序清单2.1 平移变换

  注意,程序清单2.1是对一个代表了某些对象上的点集的坐标流进行操作。

2.3、缩放

  另一个要考虑的重要的变换是缩放。它被定义为在点的距离之间成比例地扩大和缩小。或者,我们也可以把缩放看作空间本身的扩大和缩小。(参看图2.5)

图2.5 缩放变换


  这种变换一个显而易见的用处是把一个A×A维的对象放到B×B大小的窗口中。显然,对象上的每个点为了适应窗口要进行适度的缩放。在这种情况下,缩放是通过乘上一个常量完成的。也许,我们也要考虑对窗口的大小进行缩放来装入对象。
  通过缩放变换这种定义,它改变了点之间的距离,因此这不是一种刚体变换。
  总地来说,我们可以为每条坐标轴导入单独的缩放因子,执行所谓的非一致缩放而不是一致缩放,后者在每个方向上的因子都是一样的。在这两种情况中,如果因子大于0或小于1,对象被缩小。如果因子大于1,对象被放大。
  缩放实现的方法同平移例程非常类似。(参看程序清单2.2)

程序清单2.2 缩放变换


2.4、在平面内旋转
  在讨论的三种变换中,旋转有一点复杂。首先让我们考虑一下旋转变换的2D情况。这里面要涉及到三角学,尤其是sincos函数。用于直角三角形的sin(x)和cos(x)函数的定义如图2.6所示:

图2.6 直角三角形


  其中

  


  在了解了sin(a )和cos(a )之后, 我们就可以根据已知的l来计算x和y分量。

  


  这可以被看作是在已知与坐标原点距离的时候确定该点在两个正交坐标轴上的投影。(参看图2.7)

图2.7 点在坐标轴上的投影


  让我们考虑一下图2.8的情形,我们以顺时针把坐标系旋转了a 角。我们必须找出点A以原有坐标系XY中的坐标(x,y)表示的在变换后的新坐标系X'Y'中的坐标(x',y')

图2.8 旋转坐标系


  使用上面提到的sin(x), cos(x)公式,我们能够找出x和y(x和y是原始的参考坐标系中的坐标)在新坐标轴X'和Y'上的投影。把x和y在X'轴上的投影相加,同样把x和y在Y'轴上的投影也相加,这两个和就是我们要找的点A在新坐标系X'Y'上的坐标,公式如下:


  请注意符号,从图2.8中可以简单地看出,在该情况下,x被投影到Y'的负轴上。这就是为什么要加个负号的原因。
  另外,我们也可以这么考虑,把旋转变换看作点A本身的旋转,即沿着原点以顺时针旋转,而参考系固定。

2.5、3D旋转
  在上一节我们已经推出了表示平面旋转变换的公式。在3D空间中,我们可以把上面的公式看作是3D变换的特例,即3D空间中的z坐标不受影响,保持不变。因此,我们就可以用同样的方式推出3D空间中的旋转变换公式。
  如果我们在对象上固定一个点作为坐标系的原点,则对象任何围绕着这个点的方位变化都可以被看作是围绕着以该点为原点的坐标系的坐标轴的旋转。换句话说, 围绕这某个点的3D旋转可以通过按一定次序围绕这3个坐标轴旋转来实现,这样,坐标系每个连续的旋转变化都可以从前一阶段得到。
  在推出3D旋转公式时要先考虑几个影响公式的重要问题:

  • 我们使用哪一种参考系?
  • 哪个方向是旋转的正方向?
  • 旋转以什么顺序应用?

  在下面的小节中,我们要推出一系列公式,而且将要看到为什么上面几个问题影响公式的形式?

  2.5.1 坐标系
  一般地,我们不必被参考系的几何体所约束,可以自由地选择形成坐标系的坐标轴的方向。不同的科学和工程上的分支,都有着多一些或少一些的开发方便性可用来定制我们的坐标系。在大多数情况下,我们选择的要么是左手坐标要么就是右手坐标(参看图2.9)。在这本书中,我们大多数时候使用左手坐标系的符号。

图2.9 右手坐标系和左手坐标系


  但是,如果我们确实不想这么做,那我们也可以不遵循这种习惯,除此之外,应用程序可能因为某个理由选择它自己特别的参考系。按照惯例,比如说,Y轴的正方向指向上。但是,要记住,在大多数典型的位图中,Y轴指向下(这是由显示硬件中的显存布局决定的),这也许是在3D空间中选择相应坐标系的部分原因。也有人说,这是为了在飞行模拟器这类的应用程序中测量高度更自然。自然地选择坐标系可以防止我们在调试程序的时候搞糊涂了,实际上也很节省时间。
  我们在这里选择的坐标系是左手坐标系:Y轴向上,X轴向右,Z轴朝向外(屏幕内)。
  我们还要必须定义旋转角。习惯上把XY面绕Z轴的角(a )称之为倾斜(roll),ZY绕X轴的角(b )称之为俯仰(pitch),ZX绕Y轴的角(g )称之为偏转(yaw)。(参看图2.10)

图2.10 旋转角的正方向


  2.5.2 变换次序

  定义旋转变换应用的次序是非常重要的, 因为在空间中的点可能被不一定在同一个位置上g -b -a角度所改变,点被同样的角度以不同的次序改变,产生的效果可能不一样。这就是说:旋转的连续应用不能互换。其原因在于我们的假定,每个下一次变换在点已经被进行了面旋转以后进行。换句话说,我们指定的是围绕着移动了的坐标轴的角度。在图2.11中,我们可以看到a -b 旋转与b -a 旋转对对象产生的不同位置效果。

图2.11 连续旋转


  这个事实告诉我们,必须确定执行哪一种连续的旋转。也就是说,确定2D旋转应用的顺序。让我们思考一下调整周围世界的途径:首先是方向,然后我们可以把头向上或向下,最后从这个位置再左右摆动。当我们抬头或低头的时候,我们已经选定了方向。在我们的参考系中,按照如下的方向旋转:首先是偏转(yawg 角, 然后是俯仰(Pitchb 角,最后是倾斜(rolla 角。
  顺序就是上面所描述的那样,但是当然,世界中个别的旋转应用取决于观察者的方位改变。换句话说,旋转轴在观察者眼睛的中心。如果我们在世界中规定对象的坐标时,把旋转轴同对象本身联系起来,则连续的旋转顺序经常不相同,俯仰对象取决于对象被倾斜到哪一边,最后才进行偏转。
  当然,组合旋转的公式的出处是相同的。让我们来看看在图2.12中描述的第一种情况。

图2.12 三个连续的旋转


  在3D旋转中描述了9个公式。我们可以对坐标应用公式最后就得到了。其中涉及到了12个乘法。一个显而易见的问题是:能不能减少乘法的数量呢?让我们来试试通过去掉临时变量来修改一下公式。首先,得到以x,y,z表达的


  然后,使用上面的表达式,我们可以直接地通过使用x,y,z来表达


  这些公式集看起来比我们先前的公式要复杂。这里有了更多的乘法。但是,如果我们仔细地看看最终得到的公结果,我们会看到所有在方括号中的系数可以只被计算一次,这样点变换看起来就是这个样子的。


  这个计算有9个乘法,当然,找出所有的因子需要另外16个乘法。


  但是,如果我们在旋转变换中需要遍历100个点,原始的方法需要做个乘法。新方法只需个乘法,因为进行同样变换的点的变换系数只需被计算一次。
  假定我们已经把乘法的数量减至最少,我们就应该考虑加速表达式中其它的运算:即三角函数的计算。sin(x)和cos(x)可以通过级数近似计算得出。支持浮点运算的现代处理器可能对三角函数的计算有特别的内部支持。即便是没有这些支持,也很容易地通过软件来完成。许多函数都可以表示为泰勒(Taylor)级数的形式:

  


  这个表达式可以为找出在附近的的值。但它需要知道在点上的导数值。比如,的一阶导数,的二阶导数,等等。为任意的计算导数是我们要进行的稍微复杂一点的工作。为了避免这个问题,我们把的值设为0,这样就得到了麦克劳林(Maclaurin)级数:


  当的时候,的值是0或者± 1,为这种特别的级数获得计算三角函数的表达式并不困难。

  


  还要指出的是,这种形式的级数是无限级数,因此为了特定的目的,我们通过级数的有限形式来近似得到函数值:

  

  这种近似没有超出精度要求。计算只在紧靠0的附近进行。参见图2.13,注意只在很窄的区间内近似等于

图2.13 近似的sin(x)


  一般地,这种近似在从(-45° to 45°)的范围内工作的十分好。对来说, 类似的近似公式是。(参见图2.14)

图2.14 近似的 cos(x)


  很明显,我们同样要知道在窄区间之外的三角函数值。为了做到这一点,我们要么增大边界,要么就利用的周期性。在(360°)的周期内函数值重复,因此这就足以使我们能够在从0到的区间内为全部无穷多的自变量计算函数值。
   从图2.13和图2.14中, 不难看出,看起来是水平移动的。实际上,。因此, 在区间内的,可以作为来计算,级数的形式是:,同样,在区间内的值也就是可以被表示为:。(参见图2.13)按照这种方式,我们就能够在0到(360° )的范围内计算任意值。
  知道了如何计算三角函数以后,再让我们看看能不能在其它方面做一些优化工作。举例来说,既然在许多情况下的应用对角度的测量是相当粗略的,那么只考虑整数角度很有意义。在这种情况下,我们不需要整个实数范围内的sin(x)、cos(x)值,把函数区间限制在离散的360个整数中。我们在进行任何旋转之前,只对全部的360个角度进行一次计算。这些做完之后,sin(x)或cos(x)值可以通过一个简单的查询表来得到,这样就不必进行相对复杂的级数计算。
  而且,在一些应用中,我们不需要全部的360个角度。这个测量度不是很方便。稍微更有效一些的办法是把整个圆周分割为256个伪角度。通过这个办法,我们只需定义一个unsigned char(一个字节)来存储一个角度。一旦我们越过了255,值会自动地回绕为0。这种处理节省了在使用360度时可能需要的条件声明。
  为3D旋转变换编写代码是非常直截了当的。在程序清单2.3中完成的函数构建了一个旋转系数集。注意,浮点变量:T_mx1, T_mx2等,假定为全局的。

程序清单2.3 计算旋转因子


  另一个函数:T_rotation,在程序清单2.4中给出, 使用了在T_set_rotation函数中计算出的因子来执行旋转变换。

程序清单2.4 旋转变换


2.6、矩阵形式表达的变换

  矩阵是一个数值表:

 


  矩阵特例是行向量和列向量:

  


  行向量乘以列向量得到一个标量值:


  一个矩阵能够乘上其它的矩阵。得到的矩阵的每一个元素是第一个矩阵的行元素和第二个矩阵的列元素分别相乘的结果。矩阵要能够相乘,第一个矩阵的行向量和第二个矩阵的列向量必须有相同的维数。这样得到的矩阵的维数就是第一个矩阵的行×第二个矩阵的列。矩阵乘法可以用下面的方法计算:

  


  既然点是由三个坐标指定的,因此我们对一个行向量乘以一个3×3的矩阵尤其有兴趣:


  如果我们仔细地看上面的表达式, 就能够知道在上一节中推出的9个旋转因子可以很方便地在3×3矩阵中应用
。我们可以把3D旋转变换认为是一个向量乘以一个“旋转”矩阵得到的结果向量。
  同样,我们也可以通过矩阵形式表达旋转和缩放变换。3阶矩阵形式的缩放矩阵可以写成:


  矩阵形式表达的平移变换有一些困难。通常的矩阵乘法不考虑我们在这种情况下所需要的加法。但是,我们可以使用4×4矩阵以及调整坐标向量来正确地表达平移变换。平移有时候也被称作“仿射变换”,而旋转和缩放是“线性变换”。普通的仿射变换可以被认为是线性变换(在3D空间中由3×3矩阵指定)和平移(由附加的矩阵维指定)的组合。
  下面的4×4矩阵表达了平移变换:


  如果我们在形式上选择矩阵表示变换,并打算使用平移的话,其它的变换也能别表示为4×4矩阵。这时候要为旋转和缩放矩阵增加一个新的维,为了保持操作的正确性,增加的新元素中除了在对角线上的元素外,都必须等于零,对角线上的元素设为1。
  根据矩阵乘法规则,也有可能右乘一个列向量,方法如下:


  这种乘法也在形式上用于表达变换。在行向量和列向量上没有什么本质的区别,仅仅是个人的习惯问题决定使用第一种还是第二种。实际上, 行向量是列向量的转置矩阵,反之亦然。(一个矩阵的元素在转置矩阵中是):


  除了数学上的完美外,这种方式在矩阵处理上也有很多优势。它考虑到了给定的变换表达式,并且对有很多连续变换的情况给出了计算捷径:


  上式中的[A]和[B]是变换矩阵,[X]是要变换的向量。既然矩阵乘法符合下面的结合律:


  我们把上面的式子表示为:


  这样就可以先得到“级联”矩阵,接下来,坐标向量的每个变换就可以按照下面的形式计算:


  应该注意到,每个坐标轴的旋转表达为3×3的矩阵形式,我们可以使用级联矩阵的计算导出3D旋转的公式。在种意义上,其本质上做的是找出系数的表达式。我们从每个旋转的表达式开始,后面的导出基本是模仿矩阵乘法。


  如果我们随着级联矩阵的构成追踪表达式的形成过程,我们看到的是与2.5.2节非常类似的结果。
  我们已经演示了旋转变换不能交换。这就是说,不同的旋转变换顺序可以得到不同的结果。这在矩阵乘法中有很明显的反应。不象传统的整数或实数乘法,它们符合交换律,而矩阵乘法不符和交换律
  考虑


  而下面式子中的a,b,c,d,i,j,u,v 与上式中的大多不相同:


  举例来说,我们设上面式中而其它的值为0,这样我们得到:


  在有很多连续变换的情况下,矩阵形式是非常有效的。举例来说,在有多个连续变换的时候,我们计算级联矩阵,并用它来变换多点,这么做减少了许多个乘法,导致速度更快。
  但是,我们必须注意到,矩阵表达式泛化了变换。3×3矩阵与三阶向量的乘法中有9个标量乘法。另一方面,由两个公式所表达的围绕着一个坐标轴的个别旋转变换则只有4个(在这里,5个矩阵元素是0,但是,不能防止处理器在它上的循环开销)。
  解决这种问题的一个通用办法是,当我们提前知道了要执行的变换,并且表达式中多是稀疏矩阵时,我们可以预先计算公式中的级联矩阵系数,而不是通过每个单独的矩阵乘法来计算这个矩阵。在3D旋转中,直接计算系数需要16个乘法, 但两个矩阵乘法需要个乘法(每个元素需要3个乘法,一共有9个元素,要乘两次),另外,这再一次归功于在这种情况下矩阵中的大多数元素是0。
  矩阵表现形式对表达变换提供了一个非常统一的结构。当应用程序在运行中需要任意的变换集时,可以直接以矩阵的形式表达每个变换。另一方面,当有限数量的变换以及其顺序已经提前预知的时候,我们可以利用更特殊的方法的优势,比如说直接地预计算系数。

2.7、投影变换
  我们模拟的世界是三维的。但是,展现这个世界的屏幕只有两维。把3D世界坐标映射到2D屏幕坐标的过程叫做投影。
  尽管有许多能够想到的线性或非线性的方式可以把3D空间映射到2D平面上,但我们最感兴趣的两种方式是平行(正交)投影和透视投影。在下面的部分我们讨论这两种变换。

  2.7.1 平行投影
  当我们去掉3D空间中的一个维数的时候,就得到了平行投影,这时候,在3D空间中的所有点,都在从3D空间映射到2D平面的平行线上,因此,称之为平行投影。(参看图2.15)

图2.15 平行投影


  我们可以在投影线与投影面交叉的角度的基础上更进一步地细分平行投影。如果角度是直角,称之为正交投影(orthographic),否则就叫做倾斜投影(oblique)。
  典型的,可能同工程绘图一样,通常基于多正交投影集(顶视,前视和侧视)。我们最感兴趣的是正交投影,这可能是因为它与我们如何看到距离我们同样远的对象相一致。
  如果我们选择的投影面平行于坐标系的XY平面,投影线将平行于Z轴,因此, 平行投影变换将去掉所有空间中的点的z坐标。对我们来说, 在大多数的时间里,对由观察者在世界中的位置和方位(通常指的是摄像机位置和方位)创建的投影感兴趣。在大多数这类情况中,投影面无需平行于坐标系的XY面。
  也有很多方式用来描述摄像机。我们可以指定在3D世界中的观察者(摄像机)点的坐标以及指定描述观察者在世界参考系中的方位的向量。或者,不用指定向量,我们可以通过3个角度a, b ,g 表达同样的内容,描述观察者的方位。任意投影变换都能以所有的这些摄像机表达方式,通过两个步骤表现出来。在这个过程中,首先是执行仿射变换,这一步是对空间进行变换以使投影面映射到XY面上,然后,第二步,执行上面讨论的简化的平行投影。
  很显然,在第一部中应用的仿射变换将要包括把观察者平移到世界的中心,并且根据观察者的方位旋转世界。在第五章我们会回到这个问题上来,讨论观察过程的细节和指出如何找出在第一步中要用到的用于以不同的方式描述的摄像机的仿射变换。
  正如我们刚刚看到的,对平行投影来说,基本的原理是去掉z坐标。但是,去掉了z坐标后,我们丢失了所有的原始3D空间深度信息。为了减少这种影响,我们应该考虑一下透视投影。尽管有这个缺点,平行投影还是在许多领域广泛地得到了应用,比如说在CAD中的应用。平行投影保留了图像中的平行线和对象的实际大小,这个重要性质比真实的观察更重要。

  2.7.2 透视投影
  
透视投影创建的对象投影图像的大小依赖于对象与观察者的距离。在透视投影中显现这种效果并不困难。可能,最先与我们联系起来的是一个空荡荡且很直的街道,以及街道两旁消失在无穷远处的建筑物。透视投影使我们以场景现实主义的特点产生图像。毕竟,如果道路不是汇集到一点,建筑物不是在远方变得越来越小的时候,即便是同样的街道也看起来非常不自然。
  我们可以通过把观察者的眼睛仿真为一个点,而光线从所有对象反射回来汇聚到这个点上,来模拟透视投影变换。每条光线,在射到眼睛里之前,已经与观察者面前的平面相交叉。如果我们能够找到交叉横断面,并标绘那里的点,观察者的注意力会被欺骗,认为从标绘的点那里发射出的光线实际是来自与空间中其原始的位置。(参见图2.16)

图2.16 透视投影


  我们可以与前面提到的方法一样,选择一个与参考系的XY面平行的投影面。在这种情况下,我们可以注意到一些与原点和图像上的点相联系的简单关系。(参看图2.17)

图2.17 透视投影几何


  首先看一下平面的情况。观察者的眼睛位于参考系的原点。观察者的眼睛与投影面的距离称之为“焦点距离”。其目的是确定哪些点在光线从A点发射到观察者的眼睛的时候横断投影面 。我们必须在屏幕的的那个位置上标绘像素。明显地,我们遇到了另一个类似的处理三角形的问题。要注意到这么两个事实:大小两个三角形在坐标系的起点是相同的,以及两个三角形的正切值是相同的。我们有:


  对Y有同样的公式。和起来,这两个公式这么描述3D情形:


  上面我们考虑的情形只用于观察者位于坐标系原点,沿着Z轴看的情况。如果不是这种情况,与平行投影一样,我们必须先进行一个仿射变换,把原始的空间转换到在这里描述的简单透视变换的情况。在第五章讨论这种仿射变换。
  z坐标的位置必须得到某种程度的澄清。在透视变换之后, 我们将要把图元渲染到平面屏幕上。需要x和y坐标,但是不需要z坐标,这样我们可以去掉z。然而,当我们试图渲染具有多个表面的对象时,要知道所有多边形的z深度,这样,就可以推论出什么是可见的,什么是不可见的(细节在后面几章中讨论)。我们令z'=z,可以保留深度值。但是,由于x和y进行了变换,而z不变,这样,实际上深度可能发生了相对变化。这就是说, 如果原始空间中一个多边形遮掩另一个多边形,在经过投影后,实际上前者出现在后者的后面,被后者所遮掩。(参见图2.18)

图2.18 在投影变换前后的深度关系


  为了避免这种不受欢迎的效果,我们同样可以对z应用非线性变换。比如说,,这里的C是常量。
  透视效果的实现是非常直截了当的。然而,既然计算随着焦点距离变化,我们必须理解它的物理意义,这样才
能选择合适的数值。(参见图2.19)

图2.19 焦点距离含义


  焦点距离确定了视角的区域。焦点距离小则视角宽,焦点距离大则视角窄。这有助于把距离作为屏幕像素的度量。比如说,如果选择的焦点距离是160像素,显示分辨率是320×320像素,则视角是90度。
  透视变换产生的图像可能起先看起来有点不自然的失真。必须采取点措施改善其真实度。在实践中,视角在75-85度时的焦点距离效果比较好一些。当然,这也取决于场景和屏幕的几何结构。
  以矩阵的形式表达透视变换非常困难。实际上,通常的矩阵乘法只能处理线性变换。然而透视变换是非线性的。为了把它表达为矩阵形式,我们需要另一种特殊的约定。对平移我们已经使用了一种较小的约定。即在3×3矩阵上增加了一个额外的维。
  这种能够表达透视变换的约定是为了保留齐次(homogeneous)坐标。 在X,Y,Z后增加到坐标向量里的1的值要保持为1。如果在应用变换期间,该位置上得到不同的值, 向量会被重新归一化:即在向量中的每个元素都乘上一个常数,最后的元素又成为1。假定使用这种策略,我们能够把透视变换表示为如下的矩阵形式:


  既然在结果向量的最后一个元素不是1,通过乘上来归一化:


  取决于对z坐标的需求,当我们不再需要z坐标的时候,可以去掉它,上面的矩阵形式有一些变化:


  向量变为(z坐标为0):


  而且,如果观察者的位置由于某些位置的z为负值,不便于被选做坐标系的原点,比如说,变换公式和变换矩阵会因此产生不同的形式。我们应该依赖于特别的应用(在这些应用中必须使用变换)而定义必要的形式。
  一般地,我们能够以收敛点数区分各种透视变换(一点透视,二点透视),或者也可以摄像机观察方向和屏幕法线的角度来区分。我们只考虑观察方向垂直于投影面的一点透视,因为它洞察了透视变换的特殊之处。为了应用的目的,我们也许需要稍微复杂一点的透视,这在大多数情况下,很容易就引出了我们已经讨论过的先进行仿射变换的情况。
  透视变换还有几个其它的问题。比如说,z=0的点将被投影变换映射到哪里?要知道,从上面的公式中能看出,z=0的时候会导致计算错误。
  另一个问题,z为负值的时候的计算会产生负坐标。我们看到的将会是对象(或对象的一部分)被翻转过头了。但是,带有负z坐标的的对象在观察面的后面,躲在了观察者的身后。这样,我们实际上看不到它了(至少是它的一部分)。
  解决这个问题的唯一的办法是保证没有无效的Z坐标 。一条实现途径是对原始的点集进行3D裁剪(在后面章节中讨论2D和3D裁剪的细节)。
  让我们在考虑一下矩阵形式表达的透视变换。上面讨论的所有的变换中,一个点要先被渲染,它必须按照接下来说的步骤做。首先,进行基于观察者位置和方位的仿射变换。在我们进行透视变换前,必须执行裁剪,除去在观察面后面的点。然后,我们再进行透视变换本身。以传统的矩阵形式表达裁剪非常地困难。正如我们所讨论过的,当我们用一个矩阵表达几个连续变换的时候,矩阵通常是有用的。然而,在这种情况下,我们需要不止一个矩阵,因为旋转和透视被裁剪隔开了。如果我们确信没有坐标在观察面之后,那么可以避免裁剪。这时候,我们可以以矩阵的形式表达每一个变换,并计算它们的级联矩阵。在其它情况中,不能避免裁剪,或许我们想把变换分隔为两个阶段,3D裁剪前和3D裁剪后。

2.8 通过定点算法实现变换
  在前面的几节中,我们使用的是浮点乘法(sin(x)和cos(x),实数函数)。然而这种计算开销非常大。考虑到3D变换极其依赖于小数(分数)乘法,找出一种可能的加速技术是非常值得的。其中一个用来代替浮点算法的就是“定点算法”。为了完成定点算法,我们使用整数乘除法。假定整数操作开销较小,利用定点算法会给我们某种更好的性能。
  在这一节中,我们讨论一下数字计算机表达和处理数值的基本原理。这对我们理解定点算法的实现非常必要。然后,我们考虑在3D应用中这种算法起作用的特殊例子。

  2.8.1 整型数表示
  理解数字计算机表示整数的方式非常有用。(参见图2.20)

图2.20 表示整数


  在任何计算系统中,多位数的数字都有不同的加权。比如说,十进制,加权是10的幂。比如102表示为:


  二进制的情形是一样的,唯一的不同之处是,加权是2的幂。因此在图2.20中表示的数值是:


  那么如何表示十进制的小数呢?对在小数点右侧的数字来说,我们把它以-10的幂递增。即:


  在这种表达形式中,小数点分隔了0次幂和负幂,不象以指数形式表示的数字,小数点的位置是不变的。让我们看看在二进制数中加入小数点的情况:(图2.21)

图2.21 定点数的表示


  与前面非常类似,我们可以知道在图2.21中表示的数字是:


  让我们找找我们能够表示的数字的范围。以十进制类推,在十进制中,以0.01步进,两位数可以覆盖到0.99。小于0.01的数字不能表示出来,除非增加小数点后的位数。对二进制来说也是这样,参见图2.21的例子,我们能表示的最小数字是二进制的0.01,即十进制的1/4。要表示更高精度的的数字需要增加二进制小数点后面的位数。
  再让我们看看负数的表示。虽然有好几种方法, 但有一种在今天最流行。它被称之为“2的补码形式”。即在最左边的数字前加负号。(参见图2.22)

图2.22 负整数的表示


  上面表示的数字是:


  这种方式的好处是无需为了适应两种补数而改变正数加减法的算法。这要归功于整数的自然溢出循环,如果在能够表示的最大值上加个1,我们会从最重要的位(最左端)获得一个进位,忽略它,数值就是确切的0。有符号表达式中的-1是无符号表达式中的最大值。当然,-1+1=0。尽管加法和减法算法不必为了适应2的补码形式的负数而改变, 但乘除法算法需要改变。这就是为什么在大多数计算机上引入了有符号乘法和无符号乘法的缘故。
  既然在2的补码表达式中最左端的位进位带来了负数加权,而且因为它有最高的幂,在上面的例子中能够表达的最小的负数是二进制的10000,即十进制的-16。所有其它的数位都有正数加权,因此能够表达的最大的正数是二进制的01111,即十进制的15。这个轻微的不对称在大多数情况下不是什么重要的问题,然而sin(x)和cos(x)函数值在1和-1之间。为了表达它,我们选择了单整数域的格式:

图2.23 单整数域定点数


  这样,由于上面提到的不对称性,有个-1(10000)值。然而,没有正1,只有一个大约数:(01111) = 1/2+1/4+1/8+1/16=15/16,(参见图2.23)。对大多数图形应用来说,使用15位所代表的小数足够接近1,不会有任何问题。然而,对某些应用来说,有时候会产生积累错误,这时候就需要整数位了。

  2.8.2、定点数运算
  我们已经讨论过,可以使用规则的正数加减法来实现对应的定点运算。对乘除法来说,则情形有点复杂。让我们考虑一下两个十进制数相乘的时候发生了些什么。举例来说,一个十进制整数被一个带有小数点的十进制数乘,结果只需要一个整数。那么:


  正如我们看到的,乘法的实际结果在小数点后有许多位,既然我们只需要一个整数,我们必须去掉小数点后面的数字,实际上是把数字右移两位(整数部分、符号以及小数部分连续存储在一个或多个字节中)。对定点二进制数来说,处理方法是同样的。比如说,如果一个整数被(二进制)小数点后有8位的数乘, 其结果在(二进制)小数点后也有8位。如果我们只对整数形式感兴趣,我们必须把数字右移8位,去掉所有的小数部分。同样的技术也用于除法。如果我们要使两个整数相除,得到是定点数结果,被除数必须向左移(增加定点域),这样就可以用一个整数去除定点数了。

  2.8.3 定点算法的实现
  有一个通用的定点算法库是非常有帮助的。然而,定点算法依赖于选择的精度。既然可以方便地在不同的应用中使用不同的精度,我们就可以选择几种不同的定点数格式,在应用程序中每个指定的地方,直接地用于某些特殊的公式。
  还有很重要的一点是是不是我们能够使用高级C结构进行所有的运算? 或者是不是有必要把运算用汇编实现?在大多数汇编中,乘法的结果有两倍的数位。既然某些位被小数占用,我们需要得到最大数量的位。另外,乘法的结果通常是放在两个寄存器中,可以用移位来代替对运算结果的调整,也能够从寄存器中得到高位进位,利用寄存器的位长有效地完成0开销的右移。但在另一方面,汇编代码在实践中移植比较困难。显然, 对特定的应用来说,这种困难或许可以被解决。
  定点算法对实现需要大量乘法的3D变换来说显然非常有用。让我们假定以整数存储点坐标。函数的结果显然是小数形式的,在变换矩阵中当然也是这样。所有的这些可以使用同样的定点数格式来存储。如果我们决定预先计算三角函数的结果并把它们存储在数组中,就有必要把浮点数转化为定点数。如果我们简单地使用定点数存储方式把浮点数赋值为定点数,小数部分会丢失。既然我们同样需要转换小数部分内容,就要尝试在赋值之前把小数位移动到整数位。纯粹的移动操作没什么意义,不能为浮点数定义。然而,右移N个二进制位与除以有同样的效果。同样地,左移N个二进制位相当于乘上。使用这一点,很容易看出,从浮点数向定点数变换需要移动的小数数量等于N个二进制位。前者在赋值为后者的表达形式之前应该被预乘上
  在使用定点算法的典型3D应用中,当我们构建变换矩阵的时候,定点数通常是被定点数乘。结果具有的小数位通常是需要的2倍。因此,我们必须通过右移调整。(参看程序清单2.5)

程序清单2.5 为构建旋转系数使用定点数


  在实现旋转变换的历程中,我们用整数乘定点系数。结果中有与定点系数相同数量的小数位。如果我们对以整数形式得到旋转变换结果感兴趣(既然我们得到这些坐标的最终目的是索引存在图像中的离散位图,这就不是不合理的,),那么,通过把结果右移,去掉所有的小数位。
  还有几个涉及到定点算法的问题。我们必须小心地注意数值运算允许的范围。如果我们使用的是32位数,并选择了16为表达小数部分,一位用于符号位,那么只有15位用于整数部分。在两个定点数乘过之后,所有的32个位实际上都是小数(结果的小数位是两个数的小数位之和)。整数部分进入了左进位的不可知的领域。既然乘法的结果有两倍于每个数字的数位,当我们以汇编编写这种运算的代码的时候,能够完全避免这个问题。然而,如果只使用高级语言实现定点算法的时候,这成了一个严重的问题。
  最后,整数乘法加移位是不是真的比浮点乘法快?尽管从算法的复杂性来看,这好象是没什么问题,但在实践上,现实中的硬件可不一定是这样。
  下面是在某种情况下对一些处理器的大概测试,比较浮点乘法和定点乘法性能。(参看表2.1)

表2.1 浮点与定点性能对比
Sparc 浮点比定点快1.3倍
Motorolla 68040 浮点比定点快1.5倍
Intel P5 (Pentium) 浮点比定点快1.6倍
rs4000 浮点比定点快1.1倍
rs6000 浮点比定点慢1.1倍
Intel 80386sx 浮点比定点慢5.1倍
 


  正如我们看到的,如今,带有快速浮点处理单元的处理器做浮点数乘法比整数快。然而,这对加法来说这不成为问题。应该再次强调,整数算法一般比浮点开销要少,然而,后者在设计上使用了更多的硅晶体,因此对乘法来说速度更快。一些速度上的增加要归因于管道线性化(pipe-linening),它只是对一系列的浮点指令改善了吞吐量(这一点,我们要承认,矩阵运算也在其中)。在移植性的方面(不算处理器使用浮点单元做整数计算而产生退化的情况),整数算法足够快;反之,不带FPU处理器的浮点乘法却非常的慢。在许多种不同的权衡中,我们主要根据算法在时间上的困难程度来选择使用哪一种算法。如果算法主要是递增的,那么定点算法是一个很好的选择,因为加法是主要的运算。另一方面,如果乘法是主要运算,就要根据处理器来选择计算方案。

  小结
  综上所述,在本章,我们讨论了如何完成基本几何变换。我们讨论的技术可以描述位置、方位和虚拟对象运动以及可见性的变化。
  后面讨论的内容,很大程度上要依赖于本章。