??前面我们做的一切都是二维的(有时只有一维),但是已经可以做出非常酷的东东了。
现在,将它们带入到下一个等级。
???? 创建 3D? 图形总是那么另人兴奋。新加入的这个维度似乎将物体真正地带入到了生活
中。如何在 Flash? 中实现 3D? 在无数的书籍和教学软件中都有介绍。但是我不打算跳过这
些内容,我们会很快地将所有基础的知识讲完。随后,将前面章节中讨论的运动效果放到三
维空间中。说得详细些,将给大家介绍速度,加速度,摩擦力,反弹,屏幕环绕,缓动,弹
性运动,坐标旋转以及碰撞检测。
???? 现在,首先要关注 sprite 影片在 3D? 空间中运动,使用透视法计算影片在屏幕上的大
小和位置。当然,sprite 本身是平面的,我们看不到它的背面,侧面,顶面或
两章,我们将学习到点,线,图形和立体图形的 3D? 建模。?
第三维度及透视法
??? 在 3D 背后最重要的理论就是超出 x? 和 y? 存在的另一个维度。这是表示深度的维度,
通常记为 z。
??? Flash? 没有内置的 z 维度,但是要想在 ActionScript 中创建它也不是件难事。实际上,
远没有我们前面章节中的内容那么复杂!
z? 轴
???? 首先,需要确定 z 最是朝哪个方向的:向内或向外。回忆一下第三章讨论的坐标系统,
它比普通的坐标系统在某些地方是相反的。y? 轴向下,而非向上,角度则是以顺时针方向而
定的,而非逆时针方向。
??? 因此,当物体远离或接近我们的时候,是否应该让物体 z? 轴上位置增加?没有必要去
比较哪个更正确。事实上,这个课题已经被讨论许久了,人们甚至为了描述这两种方法分别
给它们取了名字:左手系统和右手系统。
???? 伸出您的右手,让拇指与食指构成一个 L 形,然后将中指弯曲 90? 度,每个手指都将
指向一个维度。现在,将您的食指指向 x? 轴的正半轴,中指指向 y? 轴的正半轴。在右手坐
标系中,拇指的指向就是 z? 轴的正半轴方向。对于 Flash 而言,意味着物体远离观察者时
z? 轴将增大,临近观察者时 z? 轴将减小,如图 15-1? 所示。?
?![](http://img50.lidatong.com.cn//uploads/allimg/c20201215/071a487daf2800957f3e48ec747b2639.gif)
图 15-1? 右手坐标系
???? 如果我们用左手来试的话,得到的结果则是相反的。如图 15-2? 所示,左手坐标系。
![](http://img50.lidatong.com.cn//uploads/allimg/c20201215/071a487daf2800957f3e48ec747b2639.gif)
图 15-2? 左手坐标系
???? 下面我们使用右手坐标系为例(图 15-1)。没有理由说不能使用左手坐标系,只不过让
z? 轴向内看起来比较好。在 Flash? 中创建第三维度(z)的下一个步骤是如何计算模拟透视。
?
透视法
???? 透视法是指如何表述物体接近或远离我们时的方法。换句话讲,如何让物体看起来更近
或更远。一幅美术作品中可能有大量的表现透视的技巧,这里我们只关注两点:
■? 当物体离得远时,会变小。
■? 当物体远离时,它们会聚集到一个消失点上。
???? 大家肯定见过火车驶向地平线时的景象。当我们在 z? 轴上移动物体时,需要做两件事:
■? 增大或减小物体的比率。
■? 让物体接近或远离消失点。
????? 在二维系统中,我们可以使用屏幕的 x? 和 y? 坐标作为物体的 x? 和 y? 坐标。只需要
一对一地映射过来即可。但是在 3D? 系统中就行不通了,因为两个物体可以有相同的 x,y
坐标,由于它们的深度不同,会使它们在屏幕上有不同的位置。因此,在 3D? 空间中移动
每个物体都需要知道它们各自的 x,y,z? 坐标,这是屏幕坐标不能做到的。现在就要用到这
三个量来描述虚拟空间的一个位置。透视法将告诉我们应该将物体放到屏幕的什么位置。
?
透视公式
???? 让物体的距离更远(增加 z),基本思想是想让它缩放比率接近0,让它的 x,y? 坐标集
中到消失点的 0,0? 处。幸好,缩放的比率与汇集的比率相同。因此,我们只需要根据给定
的距离计算出这个比率,然后在这两个地方使用它即可。图 15-3? 帮助大家解释这个概念。
![](http://img50.lidatong.com.cn//uploads/allimg/c20201215/071a487daf2800957f3e48ec747b2639.gif)
图 15-3? 从侧面观察透示图
???? 我们距离对象有一段距离。有一个观察点:眼睛。有一个成象面,可以想象成电脑的屏
幕。对象与成象面之间有一段距离,这就是 z? 的值。最后,距离观察点到成象面还有一段
距离。最后这点最为重要。虽然这段距离不完全等同于摄象机的焦距,但是与它基本相似
因此我通常用变量 fl [焦距:focal length]? 表示。下面是这个公式:
?????? scale = fl / (fl + z)
scale? 值通常是介于 0.0 到 1.0? 之间的,这就是缩放和汇聚到消失点上的比率。然而,当 z
变为负数时,fl + z? 接近 0? 而缩放比例接近无穷大。
???? 拿到这个 scale? 的值能做些什么呢?假设在处理一个影片(或 Sprite? 的子类),我们将
这个值赋给影片的 scaleX? 和 scaleY。然后再用这个因数乘以物体的 x,y? 坐标,就可以算
出物体在屏幕上的 x,y 的位置。
???? 看一个例子。通常情况下 fl? 的值在 200? 到 300? 之间。我们选用 250 这个值。如果 z
等于 0? ——换句话讲,物体就在成象面上---? 那么 scale 就等于? 250 / (250 + 0)。结果等
于 1.0。这就是 scaleX? 和 scaleY? 的值(别忘了对于 scaleX? 和 scaleY? 而言, 1.0? 就意味
着 100%)。让物体的 x,y? 坐标乘以 1.0,返回的结果不变,因此物体在屏幕上的位置就等
于它自身的 x? 和 y。
??? 现在将物体向外移让 z? 等于 250。则让 scale? 等于 250 / (250 + 250),scaleX 和 scaleY
等于 0.5。同样也改变了物体在屏幕上的位置。如果原来物体在屏幕上的位置是 200,300? 那
么现在就应该是 100,150。因此,它向着消失点移动了一半的距离。(事实上,屏幕上的位
置是相对于消失点的位置而定的,大家马上会看到)。
???? 现在,将 z? 向外移动到 9750。scale? 变成 250 / 10000, scaleX? 和 scaleY? 等于 0.025。
物体将变成一个小点儿,并且非常接近消失点。
?OK,理论够了。来看代码。
?
ActionScript 透视
???? 各位也许猜到了,我还要使用 Ball? 类。当然,您也可以自由地选择自己喜欢物体,但
是我只专注于代码,将那些酷酷的图形留给大家去做。我们用鼠标和键盘作为交互。使用鼠
标控制小球的 x,y? 坐标,方向键的上下键来控制 z? 轴的前后方向。注意,因为变量 x,y? 是
由 ActionScript? 持有的,因此我们将使用 xpos,ypos,zpos? 代表 3D? 坐标。
文档类 Perspective1.as? 的代码如下:
package {
?import flash.display.Sprite;
?import flash.events.Event;
?import flash.events.KeyboardEvent;
?import flash.ui.Keyboard;
? public class Perspective1 extends Sprite {
?? private var ball:Ball;
?? private var xpos:Number = 0;
?? private var ypos:Number = 0;
?? private var zpos:Number = 0;
?? private var fl:Number = 250;
?? private var vpX:Number = stage.stageWidth / 2;
?? private var vpY:Number = stage.stageHeight / 2;
?? public function Perspective1() {
?? init();
? }
?? private function init():void {
?? ball = new Ball();
?? addChild(ball);
addEventListener(Event.ENTER_FRAME,onEnterFrame);
?? stage.addEventListener(KeyboardEvent.KEY_DOWN,onKeyDown);
? }
?? private function onEnterFrame(event:Event):void {
???? xpos = mouseX - vpX;
???? ypos = mouseY - vpY;
???? var scale:Number = fl / (fl + zpos);
???? ball.scaleX = ball.scaleY = scale;
???? ball.x = vpX + xpos * scale;
???? ball.y = vpY + ypos * scale;
? }
?? private function onKeyDown(event:KeyboardEvent):void {
???? if (event.keyCode == Keyboard.UP) {
??? zpos += 5;
???? } else if (event.keyCode == Keyboard.DOWN) {
??? zpos -= 5;
?? }
? }
?}
}
首先创建变量? xpos,zpos,fl。然后创建一个消失点(vanishing point)vpX,vpY。
记住当物体向远处运动一段距离后,就会聚在 0,0? 点。如果不进行偏移,所有物体都会向
屏幕左上角汇集,这并不我们想要的结果。将 vpX,vpY? 设置为舞台的中心点,作为消失点。
???? 接下来,在 onEnterFrame 中设置 xpos? 和 ypos? 为鼠标与消失点的偏移位置。换句话
讲,如果鼠标在中心点右面 200? 像素,x? 就等于 200。如果在中心点左面 200? 像素的位置,
则等于 -200。
???? 然后添加 keyDown? 事件的侦听,用于改变 zpos。如果方向键上被按下 zpos? 增加,
如果方向键上被按下则减小。这将使小球向着观察者更近或更远的方向运动。
???? 最后,使用刚刚介绍过的公式计算 scale,设置小球的位置与大小。注意小球在屏幕上
的位置 x,y 是根据消失点计算的,还要加上 xpos,ypos? 与 scale? 的乘积。因此,当 scale 变
得很小时,小球将汇集到消失点上。
???? 测试一下影片,开始看起来像一个简单的鼠标拖拽。这是因为 zpos? 等于 0,scale? 等
于 1.0。所以注意不到透视的存在。当按下方向键上时,小球向内滑入一段距离,如图 15-4
所示。现在当我们移动鼠标时,小球也会随之移动,但是移动的距离很小,产生了视差效应。
![](http://img50.lidatong.com.cn//uploads/allimg/c20201215/071a487daf2800957f3e48ec747b2639.gif)
图 15-4 ActionScript 透视
???? 大家也许注意到了,如果长期按住方向键下,小球会变得非常大。这是对的。如果拿起
一小块石子放到眼前,它就会像一块巨石一样大。如果继续按住方向键下,它将变成无限大,
然后又收缩回去,但是这时整个小球已经颠倒或反转过来了。小球跑到了观察点的后面。因
此,如果眼睛可以看到身背后的东西,我猜这一定是我们所看到的。
???? 用数字解释一下,当 zpos? 等于? –fl? 时,公式从? scale = fl / (fl + zpos) 变为? scale = fl /
0。在许多语言中,除以 0? 会报错。在 Flash 中,将得到一个无限大的值。如果再将 zpos 减
小,那么就是用 fl? 除以一个负数。scale? 变为负数,这就是为什么小球会颠倒并反向运动
的原因。
???? 学会了吗?解决方法只需在小球在超过某一点时将其设置为不可见的。如果 zpos? 小于
或等于? –fl,会出现问题,因此可以判断一下这个条件,并在下面这个 Perspective2.as? 中
的 enterFrame? 函数中进行处理(其余部分与 Perspective1.as 完全相同):
private function onEnterFrame(event:Event):void {
? if (zpos > -fl) {
?? xpos = mouseX - vpX;
??? ypos = mouseY - vpY;
?? var scale:Number = fl / (fl + zpos);
?? ball.scaleX = ball.scaleY = scale;
?? ball.x = vpX + xpos * scale;
?? ball.y = vpY + ypos * scale;
? ball.visible = true;
? } else {
? ball.visible = false;
?}
}
???? 注意,如果小球不可见,我们就不必考虑缩放和位置问题了。同样还要注意如果小球处
于可见的范围,就要确保它是可见的。虽然可能略些多余的设置,但这是必要的。
???? 好的,现在我们已经学习了 3D? 基础的框架。不是很痛苦吧?一定要测试一下这个影
片,能够很好地掌握它。试改变 fl 的值,观察不同的效果。这就相当于在改变照相机的镜
头。较高的 fl? 值就像一个长焦镜头,给我们一个较小的观察空间,以及较少的可见的透视。
较小的 fl? 值将给我们一个广角镜头,形成非常广阔的透视。
???? 本章剩下的部分都是前面章节中介绍过的不同的运动效果,只不过这次是三维的
速度与加速度
实现 3D? 的速度与加速度超级简单。对于 2D? 而言,我们用 vx? 和 vy? 变量表示两个
轴的速度。现在只需要再加入 vz? 表示第三个轴即可。同样,如果有 ax 和 ay? 作为加速度,
那么再添加一个 az? 变量即可。
???? 我们可以将最后一个例子改为小行星太空船这样的游戏,不过是 3D? 版的。将它变为
全键盘控制的。方向键可以提供 x,y 轴上的推进,再加入一对儿键 Shift 和 Ctrl? 用于 z? 轴
上的推进。
???? 以下是代码(同样可在 Velocity3D.as? 中找到):
package {
?import flash.display.Sprite;
?import flash.events.Event;
?import flash.events.KeyboardEvent;
?import flash.ui.Keyboard;
? public class Velocity3D extends Sprite {
?? private var ball:Ball;
?? private var xpos:Number = 0;
?? private var ypos:Number = 0;
?? private var zpos:Number = 0;
?? private var vx:Number = 0;
?? private var vy:Number = 0;
?? private var vz:Number = 0;
?? private var friction:Number = .98;
?? private var fl:Number = 250;
?? private var vpX:Number = stage.stageWidth / 2;
?? private var vpY:Number = stage.stageHeight / 2;
?? public function Velocity3D() {
?? init();
? }
private function init():void {
?ball = new Ball();
?addChild(ball);
?addEventListener(Event.ENTER_FRAME,onEnterFrame);
?stage.addEventListener(KeyboardEvent.KEY_DOWN,onKeyDown);
}
private function onEnterFrame(event:Event):void {
?xpos += vx;
?ypos += vy;
?zpos += vz;
?vx *= friction;
?vy *= friction;
?vz *= friction;
? if (zpos > -fl) {
?? var scale:Number = fl / (fl + zpos);
?? ball.scaleX = ball.scaleY = scale;
?? ball.x = vpX + xpos * scale;
?? ball.y = vpY + ypos * scale;
? ball.visible = true;
?} else {
? ball.visible = false;
?}
}
?? private function onKeyDown(event:KeyboardEvent):void {
?? switch (event.keyCode) {
??? case Keyboard.UP :
???? vy -= 1;
???? break;
??? case Keyboard.DOWN :
???? vy += 1;
???? break;
??? case Keyboard.LEFT :
???? vx -= 1;
???? break;
??? case Keyboard.RIGHT :
???? vx += 1;
???? break;
??? case Keyboard.SHIFT :
???? vz += 1;
???? break;
??? case Keyboard.CONTROL :
???? vz -= 1;
???? break;
??? default :
???? break;
?? }
?}
?}
}
???? 我们所要做的就是为每个轴加入速度和摩擦力。当六个键中有一个被按下,将会对速度
进行适当的增加或减少(记住加速度改变速度)。然后将速度加到每个轴上,最后计算摩擦
力。现在我们就有了带有加速度,速度和摩擦力的一个 3D? 物体。哇,真是一举多得。说
过这很简单。
?
反弹
???? 本节我们将讨论平面反弹的问题--换句话讲,是与 x,z? 轴充分结合的反弹,与 2D
的屏幕边界反弹相似。
?
?
?
单物体反弹
??? 3D? 反弹,同样需要判断物体何时超出了边界,然后将物体调整到边界上,把相应轴上
的速度反转。3D? 反弹唯一的不同之处在于如何确定边界。在 2D? 中,一般都是用舞台的坐
标或其它一些可见的矩形区域。在 3D? 中,就不那么简单了。这里没有真正的可见边界的
概念,除非在三维空间中绘制一个。我们将在下一章学习三维空间中的绘制,因此现在将在
不可见的随意放置的墙壁上进行反弹。
???? 我们设置的边界和以前相同,只不过现在要把它们放到三维空间中,也就意味着可以是
正的也可以是负的。还可以选择在 z? 轴上设置边界。边界大概是这样:
private var top:Number = -250;
private var bottom:Number = 250;
private var left:Number = -250;
private var right:Number = 250;
private var front:Number = 250;
private var back:Number = -250;
???? 接下来,确定物体的新位置,需要判断是否所与这六个边界产生了碰撞。别忘了我们是
用物体一半的宽度来判断碰撞的,而这个值已经存在了 Ball? 类名为 radius? 的变量中。以
下是全部 3D? 反弹的代码(可见 Bounce3D.as):
package {
?import flash.display.Sprite;
?import flash.events.Event;
? public class Bounce3D extends Sprite {
?? private var ball:Ball;
?? private var xpos:Number = 0;
?? private var ypos:Number = 0;
?? private var zpos:Number = 0;
?? private var vx:Number = Math.random() * 10 - 5;
?? private var vy:Number = Math.random() * 10 - 5;
?? private var vz:Number = Math.random() * 10 - 5;
?? private var fl:Number = 250;
private var vpX:Number = stage.stageWidth / 2;
?? private var vpY:Number = stage.stageHeight / 2;
?? private var top:Number = -100;
?? private var bottom:Number = 100;
?? private var left:Number = -100;
?? private var right:Number = 100;
?? private var front:Number = 100;
?? private var back:Number = -100;
?? public function Bounce3D() {
?? init();
? }
?? private function init():void {
?? ball = new Ball(15);
?? addChild(ball);
?? addEventListener(Event.ENTER_FRAME,onEnterFrame);
? }
?? private function onEnterFrame(event:Event):void {
?? xpos += vx;
?? ypos += vy;
?? zpos += vz;
???? var radius:Number = ball.radius;
???? if (xpos + radius > right) {
????? xpos = right - radius;
??? vx *= -1;
} else if (xpos - radius < left) {
????? xpos = left + radius;
??? vx *= -1;
?? }
???? if (ypos + radius > bottom) {?
? ??? ypos = bottom - radius;
??? vy *= -1;
???? } else if (ypos - radius < top) {
??? ypos = top + radius;
??? vy *= -1;
?? }
???? if (zpos + radius > front) {
?????? zpos = front - radius;
??? vz *= -1;
???? } else if (zpos - radius < back) {
??? zpos = back + radius;
??? vz *= -1;
?? }
???? if (zpos > -fl) {
????? var scale:Number = fl / (fl + zpos);
????? ball.scaleX = ball.scaleY = scale;
????? ball.x = vpX + xpos * scale;
????? ball.y = vpY + ypos * scale;
??? ball.visible = true;
?? } else {
??? ball.visible = false;
?? }
? }
?}
}
???? 注意,我删掉了所有按键处理的部分,只让小球以随机的速度在每个轴上运动。现在可
以看到小球按照我们旨意进行反弹,但是谁也说不上来反弹在什么东西上——正如我所说
的,这些是任意放置不可见的边界。
?
多物体反弹
???? 让更多的物体充满整个空间也是对我们看出这些墙壁会有些帮助。为了完成这个目的,
需要很多 Ball? 类的实例。每个实例都要有自己的 xpos,zpos 以及每个轴的速度。为
了让主类(main class)的结构清晰,下面创建了一个新的类 Ball3D,来看一下:
package {
?import flash.display.Sprite;
? public class Ball3D extends Sprite {
?? public var radius:Number;
?? private var color:uint;
?? public var xpos:Number = 0;
?? public var ypos:Number = 0;
?? public var zpos:Number = 0;
?? public var vx:Number = 0;
?? public var vy:Number = 0;
?? public var vz:Number = 0;
?? public var mass:Number = 1;
?? public function Ball3D(radius:Number=40,color:uint=0xff0000) {
?? this.radius = radius;
?this.color = color;
?? init();
? }
?? public function init():void {
?? graphics.beginFill(color);
?? graphics.drawCircle(0,radius);
?? graphics.endFill();
? }
?}
}
???? 我们看到,这里所做的就是加入了每个轴的位置和速度的属性。同样,将类中的属性设
置为 public 实在不是一个好的面向对象程序设计的习惯,但是现在我们只是为了能够简单
地说明公式才这么做的。在 MultiBounce3D.as 中,创建了 50? 个新类的实例。每个实例都
有自己的? xpos,vx,vy,vz。 onEnterFrame 方法循环获得每个 Ball3D? 的引用,然
后将它们传给 move? 方法。这个方法与最初的 onEnterFrame? 完成的功能相同。代码如下(可
在 MultiBounce3D.as? 中找到):
package {
?import flash.display.Sprite;
?import flash.events.Event;
? public class MultiBounce3D extends Sprite {
?? private var balls:Array;
?? private var numBalls:uint = 50;
?? private var fl:Number = 250;
?? private var vpX:Number = stage.stageWidth / 2;
?? private var vpY:Number = stage.stageHeight / 2;
?? private var top:Number = -100;
?? private var bottom:Number = 100;
?? private var left:Number = -100;
?? private var right:Number = 100;
?? private var front:Number = 100;
?? private var back:Number = -100;
?? public function MultiBounce3D() {
?? init();
? }
?private function init():void {
?? balls = new Array();
???? for (var i:uint = 0; i < numBalls; i++) {
????? var ball:Ball3D = new Ball3D(15);
??? balls.push(ball);
????? ball.vx = Math.random() * 10 - 5;
????? ball.vy = Math.random() * 10 - 5;
????? ball.vz = Math.random() * 10 - 5;
??? addChild(ball);
?? }
?? addEventListener(Event.ENTER_FRAME,onEnterFrame);
? }
?? private function onEnterFrame(event:Event):void {
???? for (var i:uint = 0; i < numBalls; i++) {
??? var ball:Ball3D = balls[i];
?? move(ball);
?? }
? }
?? private function move(ball:Ball3D):void {
???? var radius:Number = ball.radius;
?? ball.xpos += ball.vx;
?? ball.ypos += ball.vy;
?? ball.zpos += ball.vz;
???? if (ball.xpos + radius > right) {
????? ball.xpos = right - radius;
??? ball.vx *= -1;
???? } else if (ball.xpos - radius < left) {
????? ball.xpos = left + radius;
??? ball.vx *= -1;
?? }
???? if (ball.ypos + radius > bottom) {
????? ball.ypos = bottom - radius;
??? ball.vy *= -1;
???? } else if (ball.ypos - radius < top) {
????? ball.ypos = top + radius;
??? ball.vy *= -1;
?}
???? if (ball.zpos + radius > front) {
????? ball.zpos = front - radius;
??? ball.vz *= -1;
???? } else if (ball.zpos - radius < back) {
??? ball.zpos = back + radius;
??? ball.vz *= -1;
?? }
???? if (ball.zpos > -fl) {
??? var scale:Number = fl / (fl + ball.zpos);
????? ball.scaleX = ball.scaleY = scale;
????? ball.x = vpX + ball.xpos * scale;
????? ball.y = vpY + ball.ypos * scale;
??? ball.visible = true;
?? } else {
??? ball.visible = false;
?? }
? }
?}
}
运行这个文件后,可以看到小球将六个边界内的大部空间都填满了,如图 15-5? 所示,
这样我们就可以看出这个空间的形状了。
![](http://img50.lidatong.com.cn//uploads/allimg/c20201215/071a487daf2800957f3e48ec747b2639.gif)
图 15-5 3D 小球反弹?
?
(如果要转载请注明出处http://blog.sina.com.cn/jooi,谢谢)