矩阵数学
本章我们不去介绍一些新的运动、物理学或渲染图形的方法。我要给大家介绍的是矩阵(),它给我们提供了一个新的可选方案。
矩阵在 3D 系统中 3D 点的旋转,缩放以及平移(运动)中使用得非常频繁。在各种 2D 图形的变换上也很常用。您也许可以回想到 方法就是使用矩阵来设置位置,大小以及旋转比例的。
本章大家将看到如何创建一个 3D 矩阵系统,用以操作 3D 的影片并且可以看到一些 Flash 中内置的矩阵。我很庆幸现在为止还没有一处提到 Keanu [译注:基努-里维斯,尤指电影《黑客帝国》-- The ] 的电影。看看我还能坚持多久。
矩阵基础
矩阵最简单的定义是一个数字表格。它可以有一个或多个水平的行和一个或多个垂直的列。图 18-1 展示了一些矩阵。
图18-1 一个 3×3 矩阵,一个 1×3 矩阵,一个 3×1 矩阵
矩阵通常都是由一些变量来描述的,如 M。在矩阵中为表示一个特殊的单元,我们使用的变量里面通常要用行列的值作为脚标。例如,如果图 18-1 中的 3×3 矩阵叫做 M,那么 M2,3 就等于 6,因为它指向第二行,第三列。
一个矩阵的单元不仅可以包含简单的数字,也可以是公式和变量。其实电子表格就是一个大的矩阵。我们可以用一个单元保存某一列的和,用另一个单元格将这个总和乘以一个分数,等等。我们看到这样的矩阵应该非常有用。
矩阵运算
一个电子表格就像一个自由组合的矩阵,我们要处理的矩阵更加有结构,至于能用它们做什么以及如何生成都有各自的规则。
我所见过的大多数矩阵数学的教材都只介绍两种方法的一种。首先学校讲的是矩阵运算的细节,使用的整个矩阵的几乎都是一些随机的数字。我们学习这些规则,但是不知道为什么要做这些事情或所得的结果代表什么。就像在玩把数字排列成漂亮形状的游戏。
第二个方法是详细地描述矩阵的内容但是略过手工操作,如“将两个矩阵相乘得到这个结果… …”让读者不知道乘法到底是怎么算的。
为了保证大家都能了解矩阵是如何工作的,我选择一个两者兼具的方法(折衷),从介绍一些数值矩阵开始,然后描述如何做矩阵乘法。
矩阵更为通常的作用是操作 3D 点。一个 3D 点包涵了 x, y, z 坐标。我们可以简单地将它视为一个 1×3 的矩阵:
x y z
现在假设要将这个点在空间中移动,或叫做点的平移。我们需要知道每个轴上移动多远。这时可以将它放入一个转换矩阵( )中。它又是一个 1×3 的矩阵:
dx dy dz
这里 dx, dy, dz 是每个轴移动的距离。现在我们要想办法将转换矩阵加到点矩阵上面。这就是矩阵加法,非常简单。我们只需要将相应的单元进行相加形成一个新的包含了每个单元之和的矩阵。很明显,要让两个矩阵相加,它们的大小都应该是相同的。转换方法如下:
x y z + dx dy dz = (x + dx) (y + dy) (z + dz)
获得的矩阵可以叫做 x1, y1, z1,转换之后包含了该点的新坐标。让我们用实数来试一下。假设点在 x, y, z 轴上的位置分别为 100, 50, 75,要让它们分别移动 -10, 20, -35。则应该是这样的:
100 50 75 + -10 20 -35 = (100 - 10) (50 + 20) (75 - 35)
因此,当进行加法运算时,所得该点的新坐标就是 90, 70, 40。非常简单,不是吗?大家也许已经注意到了速度间的相互关系,每个轴上的速度都加到了另一个矩阵的相应位置上。公平交易嘛。
如果我们有一个较大的矩阵,那么继续使用同样的方法,匹配每个单元。我们不会去处理大于 3×1 的矩阵加法,但是我会给大家这样一个抽象的例子:
a b c j k l (a + j) (b + k) (c + l)
d e f + m n o = (d + m) (e + n) (f + o)
g h i p q r (g + p) (h + q) (i + r)
以上就是我们需要知道矩阵加法的一切。在介绍了矩阵乘法之后,我将展示如何将现有的函数使用在矩阵 3D 引擎中。
矩阵乘法
在 3D 转换中应用更为广泛的是矩阵乘法( ),常用于缩放与旋转。在本书中我们实际上不会用到 3D 缩放,因为例子中的点缩放,影片也没有 3D 的“厚度”,因此只有二维的缩放。当然,大家可以建立一个可缩放整个 3D 立体模型的更为复杂的引擎。这就需要写一些根据新的影片大小改变 3D 点的函数。这些已经超出了我们讨论的范围,但是由于缩放是非常简单的,并且使用矩阵乘法很容易实现,因此我将带大家看一下这个例子。
使用矩阵进行缩放
首先,需要知道一个物体现有的宽度,高度和深度 —— 换句话讲,它是三个轴上每个轴分量的大小。当然可以建立一个 3×1 的矩阵:
w h d
我们知道 w, h, d 代表宽度(width),高度()和深度(depth)。下面需要缩放这个矩阵:
sx 0 0
0 sy 0
0 0 sz
这里 sx, sy, sz 是对应轴上的缩放比例。它们都将是分数或小数,1.0 为 100%,2.0 为 200%,0.5 为 50%,等等。稍后大家会看到为什么矩阵是用这种形式分布的。
要知道,矩阵乘法是为了让两个矩阵相乘,第一个矩阵的列数必需与另一个矩阵的行数相同。只要符合这个标准,第一个矩阵可以有任意多个行,第二个矩阵可以有任意多个列。本例中,由于第一个矩阵有三列(w, h, d),因此缩放矩阵就有三行。那么它们如何进行乘法运算呢?让我们来看一下这个模式:
sx 0 0
w h d
*
0 sy 0
0 0 sz
矩阵的计算结果如下:
(w*sx + h*0 + d*0) (w*0 + h*sy + d*0) (w*0 + h*0 + d*sz)
删除所有等于 0 的数:
(w*sx) (h*sy) (d*sz)
非常有合乎逻辑,因为我们是用宽度(x 轴分量)乘以 x 缩放系数,高度乘以 y 缩放系数,深度乘以 z 缩放系数。但是,我们究竟在做什么呢?那些所有等于 0 的数都像被遮盖上了,因此让我们将这个模式抽象得更清晰一点。
a b c
u v w
*
d e f
g h i
现在可以看到该模式的结果为:
(u*a + v*d + w*g) (u*b + v*e + w*h) (u*c + v*f + w*i)
我们将第一个矩阵的第一行(u, v, w)与第二个矩阵每行的第一个元素相乘。将它们加起来就得到了结果的第一行的第一个元素。在第二个矩阵的第二列(b, e, h)中使用相同的方法就得到了第二列的结果。
如果第一个矩阵的行数大于 1,就要在第二行中重复上述动作,就会得到第二行的结果:
u v w
a b c
x y z
*
d e f
g h i
就得到了这个 3×2 的矩阵:
(u*a + v*d + w*g) (u*b + v*e + w*h) (u*c + v*f + w*i)
(x*a + y*d + z*g) (x*b + y*e + z*h) (x*c + y*f + z*i)
现在让我们看一些实际中用到的矩阵乘法 —— 坐标旋转。希望通过这个缩放的例子会让它看起来更加清晰。
使用矩阵进行坐标旋转
首先,要挖出我们的 3D 点矩阵:
x y z
它保存了该点所有的坐标。当然,还要有一个旋转矩阵。我们可以在三个轴的任意一轴上进行旋转。我们将分别创建每种旋转的矩阵。先从 x 轴旋转矩阵开始:
1 0 0
0 cos sin
0 -sin cos
这里有一些正余弦值,“sin 和 cos 是什么?”很明显,这就是我们要旋转的角度的正余弦值。如果让这个点旋转 45 度,则这两个值就是 45 的正弦和余弦值。(当然,在代码中要使用弧度制)现在,我们让该矩阵与一个 3D 点的矩阵相乘,看一下结果。
1 0 0
x y z
*
0 cos sin
0 -sin cos
由此得到:
(x*1 + y*0 + z*0) (x*0 + y*cos - z*sin) (x*0 + y*sin + z*cos)
整理后结果如下:
(x) (y*cos - z*sin) (z*cos + y*sin)
这句话用 大略可以翻译成:
x = x;
y = Math.cos(angle) * y - Math.sin(angle) * z;
z = Math.cos(angle) * z + Math.sin(angle) * y;
回忆一下第十章,在讨论坐标旋转时,我们会看到这实际上就是 x 轴的坐标旋转。不要惊讶,矩阵数学只是观察和组织各种公式和方程的不同方法。至此,要创建一个 y 轴旋转的矩阵就非常容易了:
cos 0 sin
0 1 0
-sin 0 cos
最后,z 轴的旋转为:
cos sin 0
-sin cos 0
0 0 1
这是一个很好的尝试,用 x, y, z 的矩阵乘以每个旋转矩阵的单位,证明所得到的结果与第十章的坐标旋转公式完全相同。
编写矩阵
OK,现在大家已经有了足够的基础将这些知识转换为代码了。下面,我们对第十五章的 .as 进行重新转换。这个类中有 和 两个方法,用以实现 3D 坐标旋转。我们要让它们以矩阵的方式工作。
从 函数开始。它会用到小球的 x, y, z 坐标,将它们放入 1×3 矩阵,然后创建一个给定角度的 x 旋转矩阵。这个矩阵将使用数组的形式表示。最后使用 函数让两个矩阵相乘,当然还需要创建这个函数!相乘后的矩阵还要用另一个数组进行保存,因为我们需要将这些数值再存回小球的 x, y, z 坐标中。下面是新版的方法:
(ball:, :):void {
var :Array = [ball.xpos, ball.ypos, ball.zpos];
var sin: = Math.sin();
var cos: = Math.cos();
var :Array = new Array();
[0] = [1, 0, 0];
[1] = [0, cos, sin];
[2] = [0, -sin, cos];
var :Array = (, );
ball.xpos = [0];
ball.ypos = [1];
ball.zpos = [2];
}
下面是矩阵乘法的函数:
(:Array, :Array):Array {
var :Array = new Array();
[0] = [0] * [0][0] +
[1] * [1][0] +
[2] * [2][0];
[1] = [0] * [0][1] +
[1] * [1][1] +
[2] * [2][1];
[2] = [0] * [0][2] +
[1] * [1][2] +
[2] * [2][2];
;
}
现在,这个矩阵乘法的函数是手工写出的一个 1×3 和 3×3 矩阵的乘法,这就是我们后面用在每个例子中的函数。大家也可以使用 for 循环创建出更为动态的可处理任何大小的矩阵函数,但是现在我要让代码保持简洁。
最后创建 函数。如果你了解 函数,那么这个函数应该非常显而易见了。只需要创建一个 y 旋转矩阵来代替 x 旋转矩阵即可。
(ball:, :):void {
var :Array = [ball.xpos, ball.ypos, ball.zpos];
var sin: = Math.sin();
var cos: = Math.cos();
var :Array = new Array();
[0] = [ cos, 0, sin];
[1] = [ 0, 1, 0];
[2] = [-sin, 0, cos];
var :Array = (, );
ball.xpos = [0];
ball.ypos = [1];
ball.zpos = [2];
}
就是这样。大家也可以创建一个 函数,由于我们的例子中实际上不需要用到它,所以我将它作为练习留给大家完成。
现在,运行一下 .as,与第十五章的版本相比,它们看上去实际是一样的。在 AS 2 中,我发现非矩阵版本的运行得更为流畅一些。原因是我们为 3D 旋转和缩放执行了非常大量的数学运算。当我们使用矩阵数学进行计算时,会产生额外的计算。在进行矩阵乘法时,我们实际是做了四次乘以零的操作,并将这四个结果与其它数值相加。这八次数学运算实际上没有任何作用。将这些操作乘以 50 个对象,每帧旋转二次,每帧就多做了 800 次额外计算!这两个版本在 AS 3 中的运行时看不出任何的不同,这就是 Flash CS3 与 AS 3 强大的证明。但是,当加入的物体越来越多时,我们就要为这些巨大的计算量付出代价。我给大家的这些代码都非常基本的。你也许可以使它更加优化一些,让性能得到提升。
即使在 3D 中不使用矩阵,我们仍可以发现它们在其它方面的用途,我将在下面一节进行介绍。在 3D 中使用矩阵是一个很好的引子,因为这样可以让大家看到它们是如何与已知公式相关联的。同样,矩阵在其它语言的 3D 制作中应用得非常之广泛,而且比我们现在的 更为有效。在这些语言中,只需付出一点点 CPU 就可以得到矩阵所带来的组织良好的代码。如果大家试图在 Flash 以外的其它软件中进行 3D 动画编程,那么就一定要使用到矩阵。还是那句话,谁知道 Flash 播放器几年后会成为什么样?终会有一天,所有的这些技术都能与 Flash 完美地结合。
类
刚刚提到,学习矩阵的一个很好的理由是它被用在许多 类的内核中。事实上,我们有一个内置矩阵类。浏览一下 Flash 帮助文档中的 flash.geom. 类,就会发现那里写得非常清楚详细。如果本章前面内容您都能理解,那么要掌握这些材料就一定没问题。文档写得非常好,我就不再浪费空间将这些内容重复一遍了,但是我会给大家一个快速的总结并举出两个例子。
矩阵主要用于对显示对象的转换(旋转,缩放和平移)。现在,任何一个显示对象(,影片剪辑,文本类等)都有名为 (转换)的属性。这是 flash.geom. 类的一个实例,它还包含有另一个名为 的属性。如果我们创建一个 类的实例,并把它赋给显示对象的 . 属性,那么它将会改变这个对象的形状、大小或位置。我们马上会看到一些具体的例子。
基本来说 类的矩阵是一个 3×3 的矩阵,形式如下:
a b tx
c d ty
u v w
其中 u, v, w 内部自动被设置为 0, 0, 1。而且它们是不可改变的,因此不需要管它们。(更为具体的解释请参见帮助文档)我们使用下述语法来创建一个新的 :
flash.geom.;
var : = new (a, b, c, d, tx, ty);
那么这些字母是什么意思呢?tx 和 ty 非常简单。它们通过改变矩阵来控制显示对象的 x 和 y 轴。而 a, b, c, d 有些难度,因为它们都相互依赖。如果设置 b 和 c 为 0,就可以使用 a 和 d,在 x 和 y 轴上缩放一个对象。如果设置 a 和 d 为 1,就可以使用 b 和 c,分别在 y 和 x 轴上倾斜一个对象。最后,可以用一种我们非常熟悉的方式来使用 a, b, c, d。在本例中,设置如下:
cos sin tx
-sin cos ty
u v w
当然,我们可以看到这里包含了一个旋转矩阵,它确实可以旋转一个物体。自然本例中的 cos 和 sin 代表我们想要旋转的某个角度的正弦和余弦值(弧度制)。让我们试验一下这个例子。
这里可见 .as,这个类中用红色正方形创建了一个简单影片。然后设置一个 处理函数,所有的动作都加在其中:
{
flash..;
flash..Event;
flash.geom.;
class {
var angle: = 0;
var box:;
() {
init();
}
init():void {
box = new ();
box..();
box..(-50, -50, 100, 100);
box..();
(box);
(Event., );
}
(event:Event):void {
angle += .05;
var cos: = Math.cos(angle);
var sin: = Math.sin(angle);
box.. = new (cos, sin,
-sin, cos,
stage. / 2,
stage. / 2);
}
}
}
这里有一个 angle 变量,每帧都会增加。代码求出了角度的正弦和余弦值并将它们赋给新的矩阵对象,以这种方式指定 。我同时还设置了平移,根据舞台的宽度和高度把影片放置到中心。新的矩阵被赋给了影片的 . 属性。测试该影片就得到了一个旋转的正方形。
现在,也许有人会问,你什么不改变影片的 属性。在一个简单的例子中使用 没有问题,这是一个更为简单的解决方法。但是,也许在一些处理多个角度、弧度、正弦、余弦的例子中,相比将一切转换回角度制并改变 值而言,像这样的矩阵赋值确实要简单很多。
再给大家一些实际的说明,让我们试一下倾斜。倾斜()意思是将物体在一个轴上进行拉伸以便使一个部分走一条路,另一个部分走另一条路。斜体字就是一个倾斜的例子。字母顶部的部分向右倾斜,而底部的部分向左倾斜。这是 Flash 中一个众所周知的一个难点,但是使用 类将会惊人地简单。如同我前面所说,设置矩阵的 a 和 d 为 1。属性 b 是 y 轴倾斜的值,属性 c 控制 x 轴的倾斜值。让我们先来试一下 x 倾斜。在 SkewX.as 中,我几乎使用了与前一个例子完全相同的设置,只不过改变了 方法中矩阵的创建。
(event:Event):void {