前面我们做的一切都是二维的(有时只有一维),但是已经可以做出非常酷的东东了。
现在,将它们带入到下一个等级。
创建 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 所示。
图 15-1 右手坐标系
如果我们用左手来试的话,得到的结果则是相反的。如图 15-2 所示,左手坐标系。
图 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 帮助大家解释这个概念。
图 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, ypos, 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
所示。现在当我们移动鼠标时,小球也会随之移动,但是移动的距离很小,产生了视差效应。
图 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, y, 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, ypos, 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, 0, radius);
graphics.endFill();
}
}
}
我们看到,这里所做的就是加入了每个轴的位置和速度的属性。同样,将类中的属性设
置为 public 实在不是一个好的面向对象程序设计的习惯,但是现在我们只是为了能够简单
地说明公式才这么做的。在 MultiBounce3D.as 中,创建了 50 个新类的实例。每个实例都
有自己的 xpos, ypos, zpos, 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 所示,
这样我们就可以看出这个空间的形状了。
图 15-5 3D 小球反弹
(如果要转载请注明出处http://blog.sina.com.cn/jooi,谢谢)