“靝埊合”通过精心收集,向本站投稿了7篇Flash基础理论课 第八章 缓动与弹性运动Ⅰ,今天小编在这给大家整理后的Flash基础理论课 第八章 缓动与弹性运动Ⅰ,我们一起来阅读吧!
- 目录
篇1:Flash基础理论课 第八章 缓动与弹性运动Ⅰ
返回“Flash基础理论课 - 目录”
很难相信我们居然用了七章才把基础的内容介绍完,现在进入第八章,这里是高级内容的起点,从这里开始内容也开始变得越来越有趣了,前面的章节都是些常用的概念与技术。从今天开始,每章只着重介绍一两种特殊的运动。
本章将介绍缓动运动(成比例速度)与弹性运动(成比例加速度),不用担心它们只是两个名词术语,这章可以快速地略读。我会给出很多例子程序,可以使大家充分了解这项技术的强大。
成比例运动
缓动(ease)与弹性(spring)联系紧密。这两种方法都是将对象(通常指 Sprite或MovieClip)从某一点移动到目标点。使用缓动运动(Easing),如同让影片滑动到目标并停止。使用弹性运动(Springing),会产生向前或向后的反弹,最终停止在目标点位。两种方法具有一些共同点:
■需要一个目标点;
■确定到目标点的距离;
■成比例地将影片移动到目标点——距离越远,移动速度越快。
缓动运动(easing)与弹性运动(springing)的不同之处在于移动的比例。缓动运动时,速度与距离成正比,离目标越远,物体运动速度越快。当物体与目标点非常非常接近时,就几乎不动了。
弹性运动时,加速度与距离成正比。如果物体与目标离得很远,再用上加速度,会使移动速度非常快。当物体接近目标时,加速度会减小,但依然存在!物体会飞过目标点,随后再由反向加速度将它拉回来。最终,用摩擦力使其静止。
下面,我们分别看一下这两种方法,先从缓动(easing)开始。
缓动(Easing)
首先说明缓动的种类不只有一种。在 Flash IDE 中,制作补间动画时,我们就可以看到 “缓动输入”(ease in)和“缓动输出”(ease out)。下面所讨论的缓动类型与运动补间的“缓动输出”相似。在本章后面的“高级缓动”一节,将会给大家一个网站连接,在那里可以学习制作所有缓动的效果。
简单缓动
简单缓动是个非常基础概念,就是将一个物体移到别处去。创建这个“运动效果”时,希望物体能在几帧内慢慢移动到某一点。我们可以求出两点之间的夹角,然后设置速度,再使用三角学计算出 vx和vy,然后让物体运动。每一帧都判断一下物体与目标点的距离,如果到达了目标则停止。这种运动还需要一定条件的约束才能实现,但如果要让物体运动得很自然,显然这种方法是行不通的。
问题在于物体沿着固定的速度和方向运动,到达目标点后,立即停止。这种方法,用于表现物体撞墙的情景,也许比较合适。但是物体移动到目标点的过程,就像是某个人明确地知道他的目的地,然后向着目标有计划地前进,起初运动的速度很快,而临近目标点时,速度就开始慢下来了。换句话讲,它的速度向量与目标点的距离是成比例的。
先来举个例子。比如说我们开车回家,当离家还有几千米的距离时,要全速前进,当离开马路开进小区时速度就要稍微慢一点儿。当还差两座楼时就要更慢一点儿。在进入车库时,速度也许只有几迈。当进入停车位时速度还要更慢些,在还有几英尺的时候,速度几乎为零。
如果大家注意观察就会发现,这种行为就像关门、推抽屉一样。开始的速度很快,然后逐渐慢下来。
在我们使用缓动使物体归位时,运动显得很自然。简单的缓动运动实现起来也非常简单,比求出夹角,计算 vx,vy 还要简单。下面是缓动的实现策略:
1. 确定一个数字作为运动比例系数,这是个小于 1的分数;
2. 确定目标点;
3. 计算物体与目标点的距离;
4. 用距离乘以比例系数,得出速度向量;
5.将速度向量加到当前物体坐标上;
6. 重复 3到5 步。图 8-1 解释了这一过程。
图8-1 简单缓动
我们先来解释一下这个过程,看看在 ActionScript. 中是怎样实现的。
首先,确定一个分数作为比例系数。我们说过,速度与距离是成比例的。也就是说速度是距离的一部分。比例系数在 0和1 之间,系数越接近1,运动速度就会越快;系数越接近0,运动速度就会越慢。但是要小心,系数过小会使物体无法到达目标。开始我们以 0.2 作为系数,这个变量名就叫 easing。初始代码如下:
var easing:Number = 0.2;
接下来,确定目标。只需要一个简单的x,y 坐标,选择舞台中心坐标再合适不过了。
var targetX:Number = stage.stageWidth / 2;
var targetY:Number = stage.stageHeight / 2;
下面,确定物体到达目标的距离。假设已经有一个名为 ball 影片,只需要从ball的x,y 坐标中减去目标的x,y。
var dx:Number = targetX - ball.x;
var dy:Number = targetY - ball.y;
速度等于距离乘以比例系数:
vx = dx * easing;
vy = dy * easing;
下面,大家知道该怎么做了吧:
ball.x += vx;
ball.y += vy;
最后重复步骤 3 到步骤 5,因此只需加入enterFrame. 处理函数。
让我们再看一下这三个步骤,以便将它们最大程度地简化:
var dx:Number = targetX - ball.x;
var dy:Number = targetY - ball.y;
vx = dx * easing;
vy = dy * easing;
ball.x += vx;
ball.y += vy;
把前面四句简化为两句:
vx = (targetX - ball.x) * easing;
vy = (targetY - ball.y) * easing;
ball.x += vx;
ball.y += vy;
如果大家觉得还不够精简,还可以进一步缩短:
ball.x += (targetX - ball.x) * easing;
ball.y += (targetY - ball.y) * easing;
在开始学习使用缓动时,也许大家会比较喜欢用详细的句型,让程序看上去更加清晰。但是当你使过几百次后,就会更习惯用第三种写法。下面,我们选用第二种句型,以加强对速度的理解。
现在就来看一下脚本动作,依然延用Ball 类。以下是文档类 Easing1.as:
package {
import flash.display.Sprite;
import flash.events.Event;
public class Easing1 extends Sprite {
private var ball:Ball;
private var easing:Number=0.2;
private var targetX:Number=stage.stageWidth / 2;
private var targetY:Number=stage.stageHeight / 2;
public function Easing1 {
trace(targetX,targetY);
init();
}
private function init():void {
ball=new Ball ;
addChild(ball);
addEventListener(Event.ENTER_FRAME,onEnterFrame);
}
private function onEnterFrame(event:Event):void {
var vx:Number=(targetX - ball.x) * easing;
var vy:Number=(targetY - ball.y) * easing;
ball.x+= vx;
ball.y+= vy;
}
}
}
试改变easing的值,观察运动效果。
下面,大家可以让小球变成可以拖拽的,与第七章所做的拖拽与抛落效果很像。在点击小球时开始拖拽,同时,删除 enterFrame. 处理函数并且用stage 侦听 mouseUp。在 mouseUp 函数中,停止拖拽,删除 mouseUp 方法,并重新开始 enterFrame。下面是文档类 Easin2.as :
package {
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
public class Easing2 extends Sprite {
private var ball:Ball;
private var easing:Number=0.2;
private var targetX:Number=stage.stageWidth / 2;
private var targetY:Number=stage.stageHeight / 2;
public function Easing2() {
init();
}
private function init():void {
ball=new Ball ;
addChild(ball);
ball.addEventListener(MouseEvent.MOUSE_DOWN,onMouseDown);
addEventListener(Event.ENTER_FRAME,onEnterFrame);
}
private function onMouseDown(event:MouseEvent):void {
ball.startDrag();
removeEventListener(Event.ENTER_FRAME,onEnterFrame);
stage.addEventListener(MouseEvent.MOUSE_UP,onMouseUp);
}
private function onMouseUp(event:MouseEvent):void {
ball.stopDrag();
addEventListener(Event.ENTER_FRAME,onEnterFrame);
stage.removeEventListener(MouseEvent.MOUSE_UP,onMouseUp);
}
private function onEnterFrame(event:Event):void {
var vx:Number=(targetX - ball.x) * easing;
var vy:Number=(targetY - ball.y) * easing;
ball.x+= vx;
ball.y+= vy;
}
}
}
缓动何时停止
在物体缓动运动到目标点时,物体最终会到达目标点并且完成缓动效果,
但是,即使不显示该对象,缓动代码仍在执行中,这一来浪费了 CPU 资源。当物体到达目标时,应该停止执行代码。判断物体是否到达目标的方法非常简单,就像这样:
if(ball.x == targetX && ball.y == targetY) {
// code to stop the easing
}
但是这里要注意一些技巧。
我们所讨论的缓动类型涉及到了著名的Xeno悖论。Xeno也是位希腊人,爱好测量实验。Xeno将运动分解为下面几个步骤:物体要从A 点到达 B 点,它首先要移动到两点间一半的距离。然后物体再从该点出发,到达与 B 点距离一半的距离。然后再折半。每次只移动离目标一半的距离,但永远无法准确地达到目标。
这个悖论听起来是非常符合逻辑的。但是很明显,我们确实将物体从一点移动到了另一点,这样看来他的说法有些问题。到 Flash 中看看,影片在 x 轴上的位置为 0,假设要将它移动到 x 轴为 100的位置。按照悖论所说,设缓动系数为 0.5,这样每次运动到离目标一半的距离。过程如下:
■从0 点开始,经过 1 帧,到达 50。
■第 2 帧,到达 75。
■剩下的距离是 25。它的一半是 12.5 ,所以新的距离就是 87.5。
■按照这种顺序,位置将变化为 93.75, 96.875, 98.4375 等等。20 帧以后,将到达 99.999809265。
从理论上讲位置越来越接近目标,但是永远无法准确地到达目标点。然而,在代码中进行试验时,结果就发生了一些微妙的变化。归根结底问题就在于“一次最少能移动多少个像素”,答案是 1/20。事实上,二十分之一像素有个特有的名字:twip (缇)。在 Flash 内部计算单位都采用twip 像素,包括所有 Sprite 影片,影片剪辑和其它舞台对象。因此,在显示影片位置时,这个数值永远是 0.05的倍数。
下面举个例子,一个影片要到达 100的位置,而它所到达的最接近的位置事实上是 99.5。再分隔的距离,就是加上 (100 – 99.95) /2。相当于加上了 0.025,四十分之一像素。超出了 twip 是能够移动的最小值,因此无法加上“半个 twip”,结果是只增加了 0 像素。如果大家不信的话,可以亲自试一下(提示:将代码放入框架类中的init 方法):
var sprite:Sprite;
sprite = new Sprite();
addChild(sprite);
sprite.x = 0;
var targ:Number = 100;
for(var i:Number = 0; i < 20; i++) {
trace(i + “: ” + sprite.x);
sprite.x += (targ - sprite.x) * .5;
}
循环20次,将影片移动离目标一半的距离,这是基本缓动应用。将代码放入for循环,只是为了测试其位置,并不在于观察物体运动。循环到第 11次时,影片已经到达了 99.95,这已经是它能够到达的最远的地方了。
长话短说,影片并非无限地接近目标,但是它确实永远无法准确地到达目标点。这样一来,缓动代码就永远不会停止。我们要回答的问题是 “哪里才是物体最接近的目标位置?”,这需要确定到目标点的距离是否小于某个范围。我发现在很多软件中,如果物体与目标点的距离相差在一个像素以内,就可以说它已经到达了目标点,即可停止缓动了。
在处理二维坐标时,可以使用第三章所介绍的公式来计算点间距离:
distance = Math.sqrt(dx * dx + dy * dy)
如果只处理一维坐标点,如只移动一个轴的位置,就需要使用距离的绝对值,因为它有可能是个负数,使用Math.abs 方法。
OK,说得很多了,来写代码吧。这个简单的文档类,演示了如何关闭缓动运动(EasingOff.as):
package {
import flash.display.Sprite;
import flash.events.Event;
public class EasingOff extends Sprite {
private var ball:Ball;
private var easing:Number = 0.2;
private var targetX:Number = stage.stageWidth / 2;
public function EasingOff() {
init();
}
private function init():void {
ball = new Ball();
addChild(ball);
ball.y = stage.stageHeight / 2;
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
private function onEnterFrame(event:Event):void {
var dx:Number = targetX - ball.x;
if (Math.abs(dx) < 1) {
ball.x = targetX;
removeEventListener(Event.ENTER_FRAME, onEnterFrame);
trace(“done”);
} else {
var vx:Number = dx * easing;
ball.x += vx;
}
}
}
}
此例中,将缓动公式分解使用,首先计算出距离,因为我们需要知道是否该停止缓动。大家应该知道为什么要使用dx的绝对值了吧。如果小球在目标点的右边,dx的值总是负的,if (dx < 1)的结果永远为真,这就会使缓动停止。而使用Math.abs,就可以判断实际距离是否小于 1。
记住,如果将拖拽与缓动相结合,要在放开小球时,将运动代码重新启用。
移动的目标
上面例子中的目标点都是单一且固定的,这些似乎还不能满足我们的要求。事实上,Flash 并不关心物体是否到达目标,或目标是否还在移动。它只会问 “我的目标在哪里?距离有多远?速度是多少?”,每帧都如此。因此,我们可以很容易将目标点改为鼠标所在的位置,只需将原来 targetX和targetY的地方,改成鼠标的坐标 (mouseX和mouseY)。以下是一个比较简单的版本(EaseToMouse.as):
package {
import flash.display.Sprite;
import flash.events.Event;
public class EaseToMouse extends Sprite {
private var ball:Ball;
private var easing:Number = 0.2;
public function EaseToMouse() {
init();
}
private function init():void {
ball = new Ball();
addChild(ball);
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
private function onEnterFrame(event:Event):void {
var vx:Number = (mouseX - ball.x) * easing;
var vy:Number = (mouseY - ball.y) * easing;
ball.x += vx;
ball.y += vy;
}
}
}
移动鼠标观察小球跟随情况,是不是距离越远速度越快。
试想还有没有其它可移动的目标。当然还可以是一个影片向着另一个影片缓动。在早先的Flash 时代,鼠标追踪者(mouse trailers)——即一串影片跟踪者鼠标的效果——曾经风靡一时。缓动就是制作这种效果的方法之一。第一个影片缓动到鼠标上,第二个影片缓动到第一个影片上,第三个再缓动到第二个上,依此类推。大家不妨一试。
缓动不仅限于运动
本书中,有很多简单的例子程序。在这些例子中,我们主要是计算影片所用变量的值。通常,使用x,y 属性控制物体的位置。不过别忘了 Sprite 影片,影片剪辑以及各种显示对象还有很多其它可以操作的属性,而且基本都是用数字表示的。所以在读例子程序时,也应该试用其它属性代替这个例子中的属性。下面给大家一些启发。
透明度
将缓动用在 alpha 属性上。开始设置为 0 ,目标设置为 1 :
ball.alpha = 0;
var targetAlpha:Number = 1;
在 enterFrame. 函数中,使用缓动可以实现影片淡入效果:
ball.alpha += (targetAlpha - ball.alpha) * easing;
若将 0和1 颠倒过来就可以实现影片的淡出效果。
旋转
设置一个 rotation 属性和一个目标 rotation。当然,还需要一个能够表现旋转对象,比如一个箭头(arrow):
arrow.rotation = 90;
var targetRotation:Number = 270;
arrow.rotation += (targetRotation - arrow.rotation) * easing;
颜色
如果大家想挑战一下,可以在 24 位色彩中使用缓动。设置好 red,green,blue的初始值,使用缓动单独表现每一色彩元素的值,然后将它们再组合成 24 位色彩值。例如,我们可以从red 缓动到 blue。初始颜色如下:
red = 255;
green = 0;
blue = 0;
redTarget = 0;
greenTarget = 0;
blueTarget = 255;
在 enterFrame. 处理函数中的每一帧执行缓动。这里只表现一个 red 值:
red += (redTarget - red) * easing;
再将这三个数值组合为一个数(如第四章介绍的):
col = red << 16 | green << 8 | blue;
最后可以在 ColorTransform. (见第四章),线条颜色或填充色中使用该颜色值。
高级缓动
现在我们已经看到简单的缓动效果是如何实现的了,大家也许正在考虑如何使用更复杂的缓动公式制作一些效果。例如,在开始时比较缓慢,然后渐渐开始加速,最后在接近目标时再将速度慢下来。或者希望在一段时间或若干帧内完成缓动效果。
Robert Penner 以收集、编制和实现缓动公式而出名。我们可以在 www.robertpenner.com 中找到他的缓动公式。在他写这些内容时 AS 3 版本还没有出现,但是用我们前面几章所学知识,将它们转化为 AS 3 版本的也是件非常容易的事。
OK,下面进入Flash 中我最喜欢的一个主题:弹性运动(Springing)。
篇2:Flash基础理论课 第八章 缓动与弹性运动Ⅲ
返回“Flash基础理论课 - 目录”
弹簧链
下面我们将几个弹性小球串联起来,在介绍缓动一节时,我们简单地讨论了鼠标跟随的概念,意思是说一个物体跟随鼠标,另一个物体再跟随这个物体,依此类推。当时没有给大家举例子,是因为这个效果现在看来有些逊色。但是,当我们在弹性运动中使用这个概念时,效果就截然不同了。
本程序的设计思想:创建三个小球,名为ball0,ball1,ball2。第一个小球,ball0的动作与上面例子中的效果是相同的。ball1向ball0 运动,ball2向ball1 运动。每个小球都受到重力的影响,所以它们都会向下坠。代码稍有些复杂,文档类 Chain.as:
package {
import flash.display.Sprite;
import flash.events.Event;
public class Chain extends Sprite {
private var ball0:Ball;
private var ball1:Ball;
private var ball2:Ball;
private var spring:Number = 0.1;
private var friction:Number = 0.8;
private var gravity:Number = 5;
public function Chain() {
init();
}
private function init():void {
ball0 = new Ball(20);
addChild(ball0);
ball1 = new Ball(20);
addChild(ball1);
ball2 = new Ball(20);
addChild(ball2);
addEventListener(Event.ENTER_FRAME,onEnterFrame);
}
private function onEnterFrame(event:Event):void {
moveBall(ball0,mouseX,mouseY);
moveBall(ball1,ball0.x,ball0.y);
moveBall(ball2,ball1.x,ball1.y);
graphics.clear();
graphics.lineStyle(1);
graphics.moveTo(mouseX,mouseY);
graphics.lineTo(ball0.x,ball0.y);
graphics.lineTo(ball1.x,ball1.y);
graphics.lineTo(ball2.x,ball2.y);
}
private function moveBall(ball:Ball,targetX:Number,targetY:Number):void {
ball.vx += (targetX - ball.x) * spring;
ball.vy += (targetY - ball.y) * spring;
ball.vy += gravity;
ball.vx *= friction;
ball.vy *= friction;
ball.x += ball.vx;
ball.y += ball.vy;
}
}
}
看一下 Ball 这个类,我们发现每个对象实例都有自己 vx 和 vy 属性,并且它们的初始值均为0。所以在init方法中,我们只需要创建小球并把它们加入显示列表。
然后在onEnterFrame函数中,实现弹性运动。这里我们调用了 moveBall方法,比复制三次运动代码要好用得多。该函数的参数分别为一个 ball 对象以及目标点的 x,y 坐标。每个小球都调用这个函数,第一个小球以鼠标的 x,y作为目标位置,第二第三个小球以第一第二个小球作为目标位置。
最后,在确定了所有小球的位置后,开始画线,画线的起点是鼠标位置,然后依次画到每个小球上,这样橡皮圈就连接上了所有的小球。注意,程序中的 friction 降为0.8 为了使小球能够很快稳定下来。
创建一个数组保存链中所有对象的引用,然后通过循环遍历数组中的每个小球并执行运动,使这个程序更加灵活。这里只需要做一些小小的改变。首先,需要两个新的变量代表数组和对象数目:
private var balls:Array;
private var numBalls:Number = 5;
在函数 init 中,使用for循环创建所有对象,并将对象引用加入数组:
private function init():void {
balls = new Array();
for(var i:uint = 0; i < numBalls; i++) {
var ball:Ball = new Ball(20);
addChild(ball);
balls.push(ball);
}
addEventListener(Event.ENTER_FRAME,onEnterFrame);
}
最后,onEnterFrame方法的变化最大。首先设置线条,将绘图起点移动到鼠标位置,再到第一个小球,然后循环为剩下的小球设置位置并连线。通过改变 numBalls 变量,我们可以加入任意多个小球。
private function onEnterFrame(event:Event):void {
graphics.clear();
graphics.lineStyle(1);
graphics.moveTo(mouseX,mouseY);
moveBall(balls[0],mouseX,mouseY);
graphics.lineTo(balls[0].x,balls[0].y);
for(var i:uint = 1; i < numBalls; i++) {
var ballA:Ball = balls[i-1];
var ballB:Ball = balls[i];
moveBall(ballB,ballA.x,ballA.y);
graphics.lineTo(ballB.x,ballB.y);
}
}
运行结果见图 8-3,大家可以在ChainArray.as找到这个类。
图8-3 弹簧链
多目标弹性运动
我们在第五章讨论速度与加速度时,曾说过如何使一个物体受到多种外力。如果每种力都是加速度,我们只需要把它们一个个都加到速度向量中去。因为弹力不过就是施加在物体上的一种加速度,因此在一个物体上添加多种弹力也是非常容易的。
下面是创建多目标弹簧的方法:我们需要三个控制点,这些点都是 Ball 类的实例,并且具有简单的拖拽功能,用它们作为小球弹性运动的控制点。小球会立即运动到点,并在两点间寻找平衡。换句话讲,每个目标都会对小球施加一定的外力,小球的运动速度就是这些外力相加的结果。
例子程序相当复杂,使用多个方法处理不同的事件。以下是代码(文档类 MultiSpring.as),看过后再进行分段讲解:
package {
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
public class MultiSpring extends Sprite {
private var ball:Ball;
private var handles:Array;
private var spring:Number = 0.1;
private var friction:Number = 0.8;
private var numHandles:Number = 3;
public function MultiSpring() {
init();
}
private function init():void {
ball = new Ball(20);
addChild(ball);
handles = new Array();
for (var i:uint = 0; i < numHandles; i++) {
var handle:Ball = new Ball(10,0x0000ff);
handle.x = Math.random() * stage.stageWidth;
handle.y = Math.random() * stage.stageHeight;
handle.addEventListener(MouseEvent.MOUSE_DOWN,onPress);
addChild(handle);
handles.push(handle);
}
addEventListener(Event.ENTER_FRAME,onEnterFrame);
addEventListener(MouseEvent.MOUSE_UP,onRelease);
}
private function onEnterFrame(event:Event):void {
for (var i:uint = 0; i < numHandles; i++) {
var handle:Ball = handles[i] as Ball;
var dx:Number = handle.x - ball.x;
var dy:Number = handle.y - ball.y;
ball.vx += dx * spring;
ball.vy += dy * spring;
}
ball.vx *= friction;
ball.vy *= friction;
ball.x += ball.vx;
ball.y += ball.vy;
graphics.clear();
graphics.lineStyle(1);
for (i = 0; i < numHandles; i++) {
graphics.moveTo(ball.x,ball.y);
graphics.lineTo(handles[i].x,handles[i].y);
}
}
private function onPress(event:MouseEvent):void {
event.target.startDrag();
}
private function onRelease(event:MouseEvent):void {
stopDrag();
}
}
}
在init方法中,创建小球并用for循环创建三个控制点,随机安排位置,并为它们设置拖拽行为。
onEnterFrame方法循环取出每个控制点,使小球向该点方向运动。然后,用控制点的坐标设置小球的速度,反复循环,从小球开始向各个控制点画线。onPress方法的内容非常简单,但是请注意 onRelease函数,我们无法知道当前拖拽的是哪个小球。幸运的是,使用任何一个显示调用stopDrag方法,都可以停止所有的拖拽,所以只需要在文档类中直接调用该方法。
我们只要改变 numHandles 变量的值,就可以轻松地设置控制点的数量。运行结果如图 8-4所示。
图8-4 多目标弹性
到目前为止,我相信大家已经有了很多的心得与体会,并且开始尝试解决一些书中没有提到的问题。如果真是这样的话,那就太好了!这也正是我写这本书的目的。
目标偏移
我们拿到一个真正的弹簧——有弹性的金属圈——然后将它的一头固定起来,另一头放上小球或其它物体,那么物体运动的目标点是哪里?难道说目标点是固定弹簧的那头儿?不,这并不实际。小球永远也到不了这个点,因为它会受到弹簧自身的阻碍。一旦弹簧变回了正常的长度,它会对小球施加更大的力。因此,目标点就应该是弹簧展开后的末端。
要寻找目标点,首先要找到物体与固定点之间的夹角,然后沿这个角度从固定点向外展开一段长度——弹簧的长度。换句话讲,如果弹簧长度是 50,小球与固定点的夹角是 45 度的话,那么就要以 45 度的夹角向外运动 50 个像素,而这个点就是小球的目标点。图8-5 解释了这一过程。
图8-5 弹簧复位
寻找目标点的代码如下:
var dx:Number = ball.x - fixedX;
var dy:Number = ball.y - fixedY;
var angle:Number = Math.atan2(dy,dx);
var targetX:Number = fixedX + Math.cos(angle) * springLength;
var targetY:Number = fixedY + Math.sin(angle) * springLength;
运行结果是,物体向着固定点运动,但会在与目标点相差一段距离时停止移动。大家还要注意,虽然我们叫它“固定点”,只是代表弹簧固定到的某个点。而不是指这个点不能移动。也许最好的方法就是看代码。
我们继续使用鼠标位置作为固定点,弹簧的长度为100 像素,
以下是文档类(OffsetSpring.as):
package {
import flash.display.Sprite;
import flash.events.Event;
public class OffsetSpring extends Sprite {
private var ball:Ball;
private var spring:Number = 0.1;
private var vx:Number = 0;
private var vy:Number = 0;
private var friction:Number = 0.95;
private var springLength:Number = 100;
public function OffsetSpring() {
init();
}
private function init():void {
ball = new Ball(20);
addChild(ball);
addEventListener(Event.ENTER_FRAME,onEnterFrame);
}
private function onEnterFrame(event:Event):void {
var dx:Number = ball.x - mouseX;
var dy:Number = ball.y - mouseY;
var angle:Number = Math.atan2(dy,dx);
var targetX:Number = mouseX + Math.cos(angle) * springLength;
var targetY:Number = mouseY + Math.sin(angle) * springLength;
vx += (targetX - ball.x) * spring;
vy += (targetY - ball.y) * spring;
vx *= friction;
vy *= friction;
ball.x += vx;
ball.y += vy;
graphics.clear();
graphics.lineStyle(1);
graphics.moveTo(ball.x,ball.y);
graphics.lineTo(mouseX,mouseY);
}
}
}
虽然我们能够看到运行结果,但却不能真正发现这项技术的特殊用处。没关系,下一节会给大家一个特别的例子。
弹簧连接多个物体
我们知道如何用弹簧连接两个物体,还知道这个点不是固定的。但是,如果另一个物体上还有一个弹簧反作用在第一个物体上,又是怎样的呢?这里有两个物体之间由一根弹簧连接。其中一个运动了,另一个物体就要向该物体移动过来。
我开始认为制作这种效果会导致死循环从而无法实现,或者至少会引起错误。但我也没管那么多,勇敢地进行尝试。结果非常完美!
虽然前面已经描述了一些策略,但这里还要细致得说一下:物体A以物体B作为目标,并向它移动。物体B 反过来又以物体A作为目标。事实上,本例中目标偏移起了重要的作用。如果一个物体以其它物体直接作为目标,那么它们之就会相互吸引,最终聚集在一个点上。通过使用偏移目标,我们就可以使它们之间保持距离,如图 8-6所示。
图8-6 弹簧连接的两个物体
下面举一个例子,我们需要两个 Ball 类的实例。分别为ball0 和 ball1。ball0向ball1 偏移运动。ball1向ball0 偏移运动。为了不去反复写偏移弹性运动的代码,我们将这些功能写到函数 springTo 中,直接调用函数即可。如果想让 ball0向ball1 运动,只要写 springTo(ball0,ball1),然后再让 ball1向ball0 运动,就写 springTo(ball1,ball0)。还要设置两个变量,ball0Dragging 和 ball1Dragging,作为每个小球运动的开关。以下是文档类(DoubleSpring.as):
package {
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
public class DoubleSpring extends Sprite {
private var ball0:Ball;
private var ball1:Ball;
private var ball0Dragging:Boolean = false;
private var ball1Dragging:Boolean = false;
private var spring:Number = 0.1;
private var friction:Number = 0.95;
private var springLength:Number = 100;
public function DoubleSpring() {
init();
}
private function init():void {
ball0 = new Ball(20);
ball0.x = Math.random() * stage.stageWidth;
ball0.y = Math.random() * stage.stageHeight;
ball0.addEventListener(MouseEvent.MOUSE_DOWN,onPress);
addChild(ball0);
ball1 = new Ball(20);
ball1.x = Math.random() * stage.stageWidth;
ball1.y = Math.random() * stage.stageHeight;
ball1.addEventListener(MouseEvent.MOUSE_DOWN,onPress);
addChild(ball1);
addEventListener(Event.ENTER_FRAME,onEnterFrame);
stage.addEventListener(MouseEvent.MOUSE_UP,onRelease);
}
private function onEnterFrame(event:Event):void {
if (!ball0Dragging) {
springTo(ball0,ball1);
}
if (!ball1Dragging) {
springTo(ball1,ball0);
}
graphics.clear();
graphics.lineStyle(1);
graphics.moveTo(ball0.x,ball0.y);
graphics.lineTo(ball1.x,ball1.y);
}
private function springTo(ballA:Ball,ballB:Ball):void {
var dx:Number = ballB.x - ballA.x;
var dy:Number = ballB.y - ballA.y;
var angle:Number = Math.atan2(dy,dx);
var targetX:Number = ballB.x - Math.cos(angle) * springLength;
var targetY:Number = ballB.y - Math.sin(angle) * springLength;
ballA.vx += (targetX - ballA.x) * spring;
ballA.vy += (targetY - ballA.y) * spring;
ballA.vx *= friction;
ballA.vy *= friction;
ballA.x += ballA.vx;
ballA.y += ballA.vy;
}
private function onPress(event:MouseEvent):void {
event.target.startDrag();
if (event.target == ball0) {
ball0Dragging = true;
}
if (event.target == ball1) {
ball1Dragging = true;
}
}
private function onRelease(event:MouseEvent):void {
ball0.stopDrag();
ball1.stopDrag();
ball0Dragging = false;
ball1Dragging = false;
}
}
}
本例中,每个小球都是可以拖拽的。enterFrame函数负责为小球调用springTo函数。请注意,这两条语句都是由两条判断语句包围起来的,目的是要确认小球目前没被拖拽:
springTo(ball0,ball1);
springTo(ball1,ball0);
springTo函数用于产生运动,函数中的所有语句大家应该都很熟悉。首先,求出距离和角度,再计算目标点,然后向目标点运动。第二次调用函数时,参数相反,两个小球交换位置,开始的小球向另一个小球运动。这也许不是效率最高的代码,但是它可以最好地表现出运动的过程。
我们看到,小球不会依附在任何固定点上,它们都是自由飘浮的。小球之间唯一的约束就是彼此保持一定的距离。这种写法最好的地方是可以很容易地加入新的物体。例如,再创建第三个小球(ball2),同时为它设置一个变量(ball2Dragging),就可以这么添加:
if(!ball0Dragging) {
springTo(ball0,ball1);
springTo(ball0,ball2);
}
if(!ball1Dragging) {
springTo(ball1,ball0);
springTo(ball1,ball2);
}
if(!ball2Dragging) {
springTo(ball2,ball0);
springTo(ball2,ball1);
}
这样就建立了一个三角形结构,如图 8-7所示。大家熟练掌握后,很快就能做出四边形结构,直到一切复杂的弹簧结构。
图8-7 一根弹簧连接三个物体
本章重要公式总结
现在来回顾一下本章的重要公式
简单缓动,长形:
var dx:Number = targetX - sprite.x;
var dy:Number = targetY - sprite.y;
vx = dx * easing;
vy = dy * easing;
sprite.x += vx;
sprite.y += vy;
简单缓动,中形:
vx = (targetX - sprite.x) * easing;
vy = (targetY - sprite.y) * easing;
sprite.x += vx;
sprite.y += vy;
简单缓动,短形:
sprite.x += (targetX - sprite.x) * easing;
sprite.y += (targetY - sprite.y) * easing;
简单弹性,长形:
var ax:Number = (targetX - sprite.x) * spring;
var ay:Number = (targetY - sprite.y) * spring;
vx += ax;
vy += ay;
vx *= friction;
vy *= friction;
sprite.x += vx;
sprite.y += vy;
简单弹性,中形:
vx += (targetX - sprite.x) * spring;
vy += (targetY - sprite.y) * spring;
vx *= friction;
vy *= friction;
sprite.x += vx;
sprite.y += vy;
简单弹性,短形:
vx += (targetX - sprite.x) * spring;
vy += (targetY - sprite.y) * spring;
sprite.x += (vx *= friction);
sprite.y += (vy *= friction);
偏移弹性运动:
var dx:Number = sprite.x - fixedX;
var dy:Number = sprite.y - fixedY;
var angle:Number = Math.atan2(dy,dx);
var targetX:Number = fixedX + Math.cos(angle) * springLength;
var targetY:Number = fixedX + Math.sin(angle) * springLength;
篇3:FLASH文字颜色缓动特效
代码详解:
[SWF(backgroundColor=0x000000,width=550,height=400)]//设置场景背景色、大小
var r:uint=0,g:uint=0,b:uint=0;//声明无符号整数型变量r、g、b,初始值都是0
var color:uint,targetR:uint,targetG:uint,targetB:uint;//声明无符号整数型变量color、targetR、targetG、targetB
var filtersCo:uint,filtersR:uint,filtersG:uint,filtersB:uint;//声明无符号整数型变量filtersCo、filtersR、filtersG、filtersB
var glowF:GlowFilter;//声明一个发光滤镜类实例glowF
var matrix:Matrix=new Matrix;//声明一个矩阵类实例matrix
matrix.createGradientBox(550,200,0,0,0);//设置矢量图渐变填充所需矩阵样式(宽550,高200,旋转0,X方向平移0,Y方向平移0)
var sh:Shape=new Shape();//声明一个矢量图类实例sh
sh.graphics.beginGradientFill(GradientType.LINEAR,[0xff0000,0xffff00,0x00ff00,0x00ffff,0x0000ff,0xff00ff,0xff0000],[1,1,1,1,1,1,1],[0,42,84,126,168,210,255],matrix);//设置渐变填充样式(线性渐变,颜色块颜色,透明度,颜色块位置,矩阵)
sh.graphics.drawRect(0,0,550,200);//在sh中画矩形(左上角坐标(0,0),宽550,高200)
sh.graphics.endFill();//结束填充
var bmd:BitmapData=new BitmapData(550,200);//声明一个位图数据类实例bmd(宽550,高200)
var bim:Bitmap=new Bitmap(bmd);//声明一个位图包装类实例bim,用于包装bmd
bmd.draw(sh);//位图bmd为矢量图sh拍照
addChild(bim);//把位图bim添加显示列表
var txt:TextField=new TextField();//声明一个文本类实例txt
addChild(txt);//把文本txt添加显示列表
txt.defaultTextFormat=new TextFormat(“华文琥珀”,60);//设置文本格式(字体,字号)注意这样设置要放在内容前面才起作用
txt.text=“多特软件站欢迎您!”;//文本内容
txt.autoSize=TextFieldAutoSize.CENTER;//文本txt自动调整大小并居中对齐
txt.x=stage.stageWidth/2-txt.width/2;//文本的X坐标,把文本放在舞台中间
txt.y=stage.stageHeight*3/4-txt.height/2;//文本的Y坐标,把文本放在纵向3/4处
addEventListener(Event.ENTER_FRAME,frame);//添加帧频事件侦听,调用函数frame
function frame(e) {//定义帧频事件函数frame
color=bmd.getPixel(mouseX,mouseY);//color获取位图鼠标处像素点的颜色
targetR=color>>16;//targetR获取color中红色通道数值
targetG=color>>8&0xff;//targetG获取color中绿色通道数值
targetB=color&0xff;//targetB获取color中蓝色通道数值
r+=uint((targetR-r)*0.03);//r每帧增加targetR与r差的0.03倍
g+=uint((targetG-g)*0.03);//g每帧增加targetG与g差的0.03倍
b+=uint((targetB-b)*0.03);//b每帧增加targetB与b差的0.03倍
txt.textColor=r<<16|g<<8|b;//把r、g、b合成颜色付值给文字txt
filtersR=0xff-r;//filtersR获取0xff与r的差
filtersG=0xff-g;//filtersG获取0xff与g的差
filtersB=0xff-b;//filtersB获取0xff与b的差
filtersCo=filtersR<<16|filtersG<<8|filtersB;//把filtersR、filtersG、filtersB合成颜色付值给filtersCo
glowF=new GlowFilter(filtersCo,1,8,8);//发光滤镜glowF实例化(颜色,透明度,X方向模糊值,Y方向模糊值)
txt.filters=[glowF];//文字应用发光滤镜
}
//============说明
var txt1:TextField=new TextField();//声明一个文本类实例txt
addChildAt(txt1,numChildren-1);//把txt添加到显示列表的最底层
txt1.defaultTextFormat=new TextFormat(“华文隶书”,20,0x00cc33);//为txt设置文本格式(隶书,30号,深绿色)
txt1.text=“全脚本文字颜色缓动效果 鼠标滑动七彩图改变文字颜色”;//文本txt内容
txt1.x=20;//文本txt的X坐标获取20
txt1.y=360;//文本txt的Y坐标获取360
txt1.width=550;//文本txt的宽度获取220
txt1.filters=[new DropShadowFilter(2,45,0x0)];//为文本添加投影滤镜(距离5,方向45度,颜色黑色)
篇4:Flash基础理论课 第十九章 技巧 Ⅱ
返回“Flash基础理论课 - 目录”
基于计时器与时间的动画
到目前为止本书的所有例子都是通过把运动代码放到onEnterFrame. 方法中并将它赋给一个enterFrame. 事件的处理函数来实现的,我一直认为这是最简单的一种方式,因为帧的概念在 Flash 中根深蒂固,它就是给我们准备的;我猜我们大多都习以为常了。
然而,对于那些从非 Flash 编程环境转来的朋友,对于这种模式可能并不习惯。对于他们来说,时序动画模型(使用Interval 或 Timer)似乎可以更加精准地控制动画。
稍后,我们要来看看“基于时间的动画”,一种即能用作帧又能用作计时器的技术。
基于计时器的动画
作为计时器动画使用的关键类,不出意料,它就是flash.utils.Timer。同时我们还需要flash.events.TimerEvent类。
使用计时器实际上与使用enterFrame. 没什么两样。只需要我们去创建一个计时器(Timer),告诉它多久“滴答响”一声,并侦听 TimerEvent.TIMER 事件,就像对 Event.ENTER_FRAME. 事件的侦听一样。哦,还要告诉计时器何时开始!接下来,计时器就会每隔一段时间广播一个计时事件,它将调用赋给它的函数进行处理。计时器触发的间隔以毫秒为单位,在创建该计时器时指定。让我们来看一个简单的例子(可在 Timer1.as 中找到):
package {
import flash.display.Sprite;
import flash.utils.Timer;
import flash.events.TimerEvent;
public class Timer1 extends Sprite {
private var timer:Timer;
public function Timer1() {
init();
}
private function init():void {
timer = new Timer(30);
timer.addEventListener(TimerEvent.TIMER, onTimer);
timer.start();
}
private function onTimer(timer:TimerEvent):void {
trace(“timer!”);
}
}
}
重要的部分加粗表示。我们创建一个计时器,告诉它每隔30毫秒触发一次,意味着每秒约 33次。添加一个事件的侦听器并将它起动。onTimer 方法与我们以前用的onEnterFrame类似。
这是我们所要知道计时器的大部分内容。它还有其它两个漂亮的特征。一个是在创建计时器时,可以通过第二个参数,repeatCount,告诉它运行的次数。假设我们要让计时器每秒运行一次,总共执行 5 秒。就可以这样做:
timer = new Timer(1000, 5);
如果没有指定重复的次数,或传入 0,那么计时器将无限地运行。
另一个好东西是可以让计时器在某个点上启动或停止,只需要调用timer.stop 或 timer.start 即可。在某些例子中,这样做比删除和重新加入事件侦听器更简单一些。
与 enterFrame. 相比,很多朋友更喜欢使用计时器的原因是,理论上讲,计时器可以让我们控制动画的速度——这是对于帧的不精确性的一个重大改进。我之所以说“理论上讲”,是因为这里有些事情需要弄清楚。
首先,实际上计时器要依赖于帧频。另一个原因是计时器的事件函数中的代码会增加整个计时器间隔。稍后我会解释一下第二点。现在,让我们看看计时器是如何与帧频相关联的。下面是文档类 Timer2.as,使用到我们著名的Ball类。
package {
import flash.display.Sprite;
import flash.utils.Timer;
import flash.events.TimerEvent;
public class Timer2 extends Sprite {
private var timer:Timer;
private var ball:Ball;
public function Timer2() {
init();
}
private function init():void {
stage.frameRate = 1;
ball = new Ball();
ball.y = stage.stageHeight / 2;
ball.vx = 5;
addChild(ball);
timer = new Timer(20);
timer.addEventListener(TimerEvent.TIMER, onTimer);
timer.start();
}
private function onTimer(event:TimerEvent):void {
ball.x += ball.vx;
}
}
}
这里我们把创建出来的小球放在舞台的左侧。让它以vx为5的速度穿过舞台。然后设置一个20毫秒的计时器,每秒约调用50次。同时设置影片的帧频为1 就是为了看看帧频是否会对计时器有影响。
测试后,我们会看到小球没有平稳地穿过屏幕,而是每秒钟跳一下 —— 以帧的频率。每跳一次都会大于5像素。为什么呢?
回想一下一、二章的动画基础,我们知道模型是需要更新的,所以屏幕要根据新的模型被刷新。这里时间间隔函数确实将更新了模型并将小球每次移动 5像素,但是只有在 Flash 进入新的一帧时才进行刷新。仅仅运行一个函数不会驱使 Flash 进行重绘。
幸运的是,Macromedia (现在的Adobe)的好人们看到了这个问题并给了我们另一个小工具:updateAfterEvent。最初是在 Flash MX 中介绍的,现在它是传给计时器事件函数中 TimerEvent 对象的一个方法。就像它的名字一样,在事件之后刷新屏幕的。当然,由于它是TimerEvent类的一个方法,所以只有在收到一个事件后才能被调用。(事实上,它也是KeyboardEvent 和 MouseEvent的方法,因此也能在这些处理函数中调用。)
这样一来,我们可以修正一下 onTimer 事件:
private function onTimer(event:TimerEvent):void {
ball.x += ball.vx;
event.updateAfterEvent();
}
现在一切有所好转了。非常流畅。但是如果您意识到小球应该每秒更新50次,我们看到的基本上应该是一个50 fps的动画。这就意味着小球的运动应该比第四章创建的fps 小于50的enterFrame. 事件的例子更为流畅。但是实际的运动更为缓慢。
问题出来了,计时器在某种程度上要依赖于帧频。通过我的测算,在帧频为1 fps 时,我们所得到的计时器运行的最快间隔大约为100毫秒。
我已经听到了嘲笑:每帧只得到了 10次间隔。所以,试将帧频改为5。它允许每秒更新50次。在我看来,仍然不是很流畅。如果不大于每秒10帧的话是不会达到真正流畅的效果。因此,我们可以看到使用计时器并不能完全让我们从帧频的铐链中解脱出来。
下一个问题是计时器内部是怎样工作的,它会对计时的精确度产生多大的影响。当 timer.start() 被调时,实际上发生了什么,Flash 等待一段指定的时间,然后广播事件,运行与该计时器相关的处理函数。只有当函数执行完成后计时器才开始定时下一次计时。看一个例子,假设我们有一个每 20毫秒运行一次计时器。假设在处理函数中有大量的代码需要执行 30毫秒。下一轮定时只有在所有的代码都运行完成后才开始。这样一来,我们的函数会在大约每 50毫秒调用一次。因为在用户的机器上没法精确地知道代码会运行多快,所以多数情况下,计时器动画不会比帧动画精确多少。
如果您需要真正的精确,那么基于时间的动画则是我们的必经之路。
基于时间的动画
如果要让动画中物体的速度是一致的,那么基于时间的动画就是我们要使用的方法。这种方法在一些游戏中比较常用。我们知道,帧和计时器动画都不能以特定的速率播放。一个复杂的动画在一台又老又慢的电脑上运行可能要比最初设计的速度慢上许多。我们马上会看到,使用基于时间的动画无论最终动画运行的帧频如何,都将获得可靠的速度。
首先要改变考虑速度的方法。到目前为止,在我说 vx = 5 时,我们使用的单位是像素每帧(pixels per frame)。换句话讲,每进入新的一帧物体都将在 x 轴上运动 5像素。在计时器动画中,当然,就应该是每次定时间隔移动 5像素。
对于时间动画,我们将使用真正的时间单位,如秒。由于我们是处理完整的一秒,而非其中的一部分,因此这个速度值就要更大一些。如果某个物体的速度是每帧10像素,每秒30帧的速度运动,大约每秒300像素。比如下面这个例子,我从第六章的Bouncing2.as文档类中截取了一部分并进行了一些变化,见下面粗体部分(也可在 TimeBased.as 中找到):
package {
import flash.display.Sprite;
import flash.events.Event;
import flash.utils.getTimer;
public class TimeBased extends Sprite {
private var ball:Ball;
private var vx:Number;
private var vy:Number;
private var bounce:Number = -0.7;
private var time:Number;
public function TimeBased() {
init();
}
private function init():void {
stage.frameRate = 10;
ball = new Ball();
ball.x = stage.stageWidth / 2;
ball.y = stage.stageHeight / 2;
vx = 300;
vy = -300;
addChild(ball);
time = getTimer();
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
private function onEnterFrame(event:Event):void {
var elapsed:Number = getTimer() - time;
time = getTimer();
ball.x += vx * elapsed / 1000;
ball.y += vy * elapsed / 1000;
var left:Number = 0;
var right:Number = stage.stageWidth;
var top:Number = 0;
var bottom:Number = stage.stageHeight;
if (ball.x + ball.radius >right) {
ball.x = right - ball.radius;
vx *= bounce;
} else if (ball.x - ball.radius < left) {
ball.x = left + ball.radius;
vx *= bounce;
}
if (ball.y + ball.radius >bottom) {
ball.y = bottom - ball.radius;
vy *= bounce;
} else if (ball.y - ball.radius < top) {
ball.y = top + ball.radius;
vy *= bounce;
}
}
}
}
如上所描述,我改变了对速度的计算,让它们使用固定的值,而非随机值,
然后我创建了一个名为time的变量,让它等于flash.utils.getTimer 函数的结果。getTimer 函数非常简单。它返回影片已经运行了的毫秒数 —— 这就是它全部的工作。我们没有办法清除它,重启它,改变它,等等。它只是一个计数器。
看起来它似乎用处不大,但是如果先调用一次 getTimer 将值保存起来,稍后再调用它一次,将两个结果相减,我们就能知道确切的——毫秒——这两次调用之间经过了多少时间。
这就是策略:在每帧的开始时调用getTimer 计算与上一帧间隔了多长时间。如果将它除以1,000,我们将得到以秒为单位的间隔时间,这是个以秒为单位的分数。由于我们的vx 和 vy 现在是以像素每秒来计算的,因此可以让它们去乘以这个分数,这样就知道要对物体移动多少了。同样,不要忘记重新设置 time 变量的值,以便让下一帧进行计算。
测试一下,我们将看到小球移动的速度几乎与最初的速度相同!但是真正令人激动的是我们可以以任何帧频来发布这个影片,它仍然可以以同样的速度运动!通过修改 stage.frameRate的值,试验一下高到1,000 fps,低到10 fps,你会看到小球的速度是相同的。当然,较高的频率会让动画更加流畅,而较低的频率则会十分不连贯,但是速度是一致的。
大家可以把这个技术应用在本书任何一个包含速度的例子中。如果这样的话,还需要将相似的技术应用在加速度或外力上,如重力,因为它们也是基于时间的。加速度肯定要比转前要大很多,因为加速度被定义为速度对时间的变化率。例如,重力大约为32 英尺/秒/秒。
如果在一个30 fps帧的动画中,重力为0.5的话,那么现在就应该是450。计算方法 0.5 * 30 * 30。然后像这样将它加入:
vy += gravity * elapsed / 1000;
在最后一个例子中加入 450 重力后测试一下。我们会看到它与帧动画中加入重力 0.5的效果是相同的。使用这种技术的一个技巧是将帧频设置得非常高,如 100。虽然没有机器能够与真正的帧频相吻合,但是基于时间的动画将保证每个人看到的影片运行得都是最流畅的。
同质量物体的碰撞
回忆一下第十一章的动量守恒。那里有非常严谨的代码。不过,当两个相同质量的物体发生碰撞时,我们实现起来可以更简单一些。基本原理是,两个物体沿着碰撞的线路交换它们的速度。同时还要用坐标旋转来决定碰撞的线路,以及物体的速度,这样就摆脱了复杂的动量守恒公式。来看看它是如何工作的,让我们回到文件 MultiBilliard2.as 中,将用它作为下一个例子 SameMass.as的基础。我就不再列出原先所有的代码了,因为它实在是一个很大的文件。但是,我们要来看一下创建所有小球的for 循环:
for(var i:uint = 0; i < numBalls; i++) {
var radius:Number = Math.random() * 50 + 20;
var ball:Ball = new Ball(radius);
ball.mass = radius;
ball.x = Math.random() * stage.stageWidth;
ball.y = Math.random() * stage.stageHeight;
ball.vx = Math.random() * 10 - 5;
ball.vy = Math.random() * 10 - 5;
addChild(ball);
balls.push(ball);
}
对于新的文件来说,要把粗体字的部分改为这一行:
var radius:Number = 30;
让所有小球大小都相同,消除了质量的概念,相当于给它们相同的质量。
接下来,看一下 checkCollision 函数。请找到这一部分:
// 旋转 ball0的速度
var vel0:Point = rotate(ball0.vx,
ball0.vy,
sin,
cos,
true);
// 旋转 ball1的速度
var vel1:Point = rotate(ball1.vx,
ball1.vy,
sin,
cos,
true);
// 碰撞反应
var vxTotal:Number = vel0.x - vel1.x;
vel0.x = ((ball0.mass - ball1.mass) * vel0.x +
2 * ball1.mass * vel1.x) /
(ball0.mass + ball1.mass);
vel1.x = vxTotal + vel0.x;
这一部分找出了碰撞线路上的速度,根据物体的质量求出碰撞的结果。“碰撞反应”部分是动量守恒的要素,这就是我们可以消除的部分。我们可以让 vel0 和 vel1 进行交换,就可以很容易地替换它了。整个代码段如下:
// 旋转 ball0的速度
var vel0:Point = rotate(ball0.vx,
ball0.vy,
sin,
cos,
true);
// 旋转 ball1的速度
var vel1:Point = rotate(ball1.vx,
ball1.vy,
sin,
cos,
true);
// 交换两个速度
var temp:Point = vel0;
vel0 = vel1;
vel1 = temp;
这里甚至还可以再优化,但是为了代码的清晰我就不做改变了。现在我们已经摆脱了一小块儿数学问题,测试一下修改前与修改后的文件,所得的结果是相同的。
声音整合
本书一直没有提到声音的使用。因为声音并不是动画的直接组成部分,优质的声音效果可以让 Flash 影片更加真实、引人入胜。
我们可以使用不同的方法来加入声音。回溯到Flash IDE 最早的程序版本,我们有一种使用声音的特殊方式 —— 将声音导入到库,再把它加入到帧里面。这不是我要介绍的方法。我将介绍在 AS 3 中使用声音的一些基础。
AS 3的Sound类有了很大的变化,而且还有几个额外的类可以帮助我们对其进行修饰。这里没有太多的空间进行深入的讨论,但是有一个方面我想应该对于我们这本书来说会很有用。这就是当动画中发生某种事件时,应该播放声音。最明显的就应该是碰撞了。一个小球碰到墙上或其它小球上,我们会听到“砰”、“啵嘤”、“啪”或其它什么声音。因此,我们需要掌握通过 ActionScript. 来启动声音的能力。
这个例子中,我们还要回到Bouncing2.as,小球会与墙壁产生碰撞。每次碰撞到墙壁时,我想让它发出声音。新的类请见 SoundEvents.as。
首先,需要有声音效果。在网上有许多免费的声音效果资源。其中最火的Flash声音网站是FlashKit。他们的音乐文件除了有 loop 以外,还有一个声音效果库 www.flashkit.com/soundfx/。这些效果被分类为Cartoon,Interfaces,Mechanical 等等,而且这个网站有超过 6,000 多个声音效果文件,所以您应该能够找到适合自己的音效。我们可以在页面上直接进行预览(preview),找到自己喜欢的文件后,以MP3 格式进行下载。将它保存到硬盘上与最终发部的影片放在同一目录下。
有时我们要重新给文件一个更为简单的名字。例如,我下载了一个“boing”声音,我就将它重命名为boing.mp3。
在 AS 3 中使用声音的基础实际上要比 AS 2 中简单一些。
首先,我们需要创建声音对象。假设在类中已经声明了一个名为boing的变量:
private var boing:Sound;
创建一个声音对象就这么简单:
boing = new Sound();
当然,如同大多数 AS 3的类一样,Sound类也在包中,flash.media 包,因此要确保先导入 flash.media.Sound。
连接一个外部声音对象最简单的方法是在构造函数中传入一个请求(request)。
就像读取外部图像(第四章)一样,我们不能直接传入外部声音文件的URL。而是要将它包装到URLRequest 中(flash.net.URLRequest,需要导入它)。应该像这样:
boing = new Sound(new URLRequest(“boing.mp3”));
全部内容就是这样。现在声音已经准备好。我们要做的就是:
mySound.play();
无论在哪儿都会播放出这个音效。在 play 中有一些可选参数,如偏移的毫秒数,以及播放的次数,但是默认情况下是从声音的起始位置播放一次声音,这已经满足了我们通常的需求。以下是SoundEvents.as的全部代码,展示了 Sound 对象的创建,无论何时小球碰撞到墙上,都会播放声音。
package {
import flash.display.Sprite;
import flash.events.Event;
import flash.media.Sound;
import flash.net.URLRequest;
public class SoundEvents extends Sprite {
private var ball:Ball;
private var vx:Number;
private var vy:Number;
private var bounce:Number = -0.7;
private var boing:Sound;
public function SoundEvents() {
init();
}
private function init():void {
boing = new Sound(new URLRequest(“boing.mp3”));
ball = new Ball();
ball.x = stage.stageWidth / 2;
ball.y = stage.stageHeight / 2;
vx = Math.random() * 10 - 5;
vy = -10;
addChild(ball);
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
private function onEnterFrame(event:Event):void {
ball.x += vx;
ball.y += vy;
var left:Number = 0;
var right:Number = stage.stageWidth;
var top:Number = 0;
var bottom:Number = stage.stageHeight;
if (ball.x + ball.radius >right) {
boing.play();
ball.x = right - ball.radius;
vx *= bounce;
} else if (ball.x - ball.radius < left) {
boing.play();
ball.x = left + ball.radius;
vx *= bounce;
}
if (ball.y + ball.radius >bottom) {
boing.play();
ball.y = bottom - ball.radius;
vy *= bounce;
} else if (ball.y - ball.radius < top) {
boing.play();
ball.y = top + ball.radius;
vy *= bounce;
}
}
}
}
测试一下影片看一看 … … 听一听拥有声音以后带来的不同感受。当然,要找到正确的声音用在正确的环境下,也不要加得太多,因为这本身也是一门艺术。
篇5:Flash教程:文字颜色缓动特效
先来看下效果:
代码详解:
[SWF(backgroundColor=0x000000,width=550,height=400)]//设置场景背景色、大小
var r:uint=0,g:uint=0,b:uint=0;//声明无符号整数型变量r、g、b,初始值都是0
var color:uint,targetR:uint,targetG:uint,targetB:uint;//声明无符号整数型变量color、targetR、targetG、targetB
var filtersCo:uint,filtersR:uint,filtersG:uint,filtersB:uint;//声明无符号整数型变量filtersCo、filtersR、filtersG、filtersB
var glowF:GlowFilter;//声明一个发光滤镜类实例glowF
var matrix:Matrix=new Matrix;//声明一个矩阵类实例matrix
matrix.createGradientBox(550,200,0,0,0);//设置矢量图渐变填充所需矩阵样式(宽550,高200,旋转0,X方向平移0,Y方向平移0)
var sh:Shape=new Shape();//声明一个矢量图类实例sh
sh.graphics.beginGradientFill(GradientType.LINEAR, [0xff0000,0xffff00,0x00ff00,0x00ffff,0x0000ff,0xff00ff,0xff0000], [1,1,1,1,1,1,1],[0,42,84,126,168,210,255],matrix);//设置渐变填充样式(线性渐变,颜色块颜色,透明度,颜色块位置,矩阵)
sh.graphics.drawRect(0,0,550,200);//在sh中画矩形(左上角坐标(0,0),宽550,高200)
sh.graphics.endFill();//结束填充
var bmd:BitmapData=new BitmapData(550,200);//声明一个位图数据类实例bmd(宽550,高200)
var bim:Bitmap=new Bitmap(bmd);//声明一个位图包装类实例bim,用于包装bmd
bmd.draw(sh);//位图bmd为矢量图sh拍照
addChild(bim);//把位图bim添加显示列表
var txt:TextField=new TextField();//声明一个文本类实例txt
addChild(txt);//把文本txt添加显示列表
txt.defaultTextFormat=new TextFormat(“华文琥珀”,60);//设置文本格式(字体,字号)注意这样设置要放在内容前面才起作用
txt.autoSize=TextFieldAutoSize.CENTER;//文本txt自动调整大小并居中对齐
txt.x=stage.stageWidth/2-txt.width/2;//文本的X坐标,把文本放在舞台中间
txt.y=stage.stageHeight*3/4-txt.height/2;//文本的Y坐标,把文本放在纵向3/4处
addEventListener(Event.ENTER_FRAME,frame);//添加帧频事件侦听,调用函数frame
function frame(e) {//定义帧频事件函数frame
color=bmd.getPixel(mouseX,mouseY);//color获取位图鼠标处像素点的颜色
targetR=color>>16;//targetR获取color中红色通道数值
targetG=color>>8&0xff;//targetG获取color中绿色通道数值
targetB=color&0xff;//targetB获取color中蓝色通道数值
r+=uint((targetR-r)*0.03);//r每帧增加targetR与r差的0.03倍
g+=uint((targetG-g)*0.03);//g每帧增加targetG与g差的0.03倍
b+=uint((targetB-b)*0.03);//b每帧增加targetB与b差的0.03倍
txt.textColor=r<<16|g<<8|b;//把r、g、b合成颜色付值给文字txt
filtersR=0xff-r;//filtersR获取0xff与r的差
filtersG=0xff-g;//filtersG获取0xff与g的差
filtersB=0xff-b;//filtersB获取0xff与b的差
filtersCo=filtersR<<16|filtersG<<8|filtersB;//把filtersR、filtersG、filtersB合成颜色付值给filtersCo
glowF=new GlowFilter(filtersCo,1,8,8);//发光滤镜glowF实例化(颜色,透明度,X方向模糊值,Y方向模糊值)
txt.filters=[glowF];//文字应用发光滤镜
}
//============说明
var txt1:TextField=new TextField();//声明一个文本类实例txt
addChildAt(txt1,numChildren-1);//把txt添加到显示列表的最底层
txt1.defaultTextFormat=new TextFormat(“华文隶书”,20,0x00cc33);//为txt设置文本格式(隶书,30号,深绿色)
txt1.text=“全脚本文字颜色缓动效果 鼠标滑动七彩图改变文字颜色”;//文本txt内容
txt1.x=20;//文本txt的X坐标获取20
txt1.y=360;//文本txt的Y坐标获取360
txt1.width=550;//文本txt的宽度获取220
txt1.filters=[new DropShadowFilter(2,45,0x0)];//为文本添加投影滤镜(距离5,方向45度,颜色黑色)
篇6:Flash基础理论课 第十八章 矩阵数学
返回“Flash基础理论课 - 目录”
本章我们不去介绍一些新的运动、物理学或渲染图形的方法,我要给大家介绍的是矩阵(Matrix),它给我们提供了一个新的可选方案。
矩阵在3D 系统中 3D点的旋转,缩放以及平移(运动)中使用得非常频繁。在各种 2D 图形的变换上也很常用。您也许可以回想到 beginGradientFill 方法就是使用矩阵来设置位置,大小以及旋转比例的。
本章大家将看到如何创建一个3D矩阵系统,用以操作 3D的影片并且可以看到一些 Flash中内置的矩阵。我很庆幸现在为止还没有一处提到 Keanu Reeves [译注:基努-里维斯,尤指电影《 帝国》-- The Matrix]的电影。看看我还能坚持多久。
矩阵基础
矩阵最简单的定义是一个数字表格。它可以有一个或多个水平的行和一个或多个垂直的列。图 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
现在假设要将这个点在空间中移动,或叫做点的平移。我们需要知道每个轴上移动多远。这时可以将它放入一个转换矩阵(translation matrix)中。它又是一个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 转换中应用更为广泛的是矩阵乘法(matrix multiplication),常用于缩放与旋转。在本书中我们实际上不会用到 3D 缩放,因为例子中的点缩放,影片也没有3D的“厚度”,因此只有二维的缩放。当然,大家可以建立一个可缩放整个3D 立体模型的更为复杂的引擎。这就需要写一些根据新的影片大小改变 3D点的函数。这些已经超出了我们讨论的范围,但是由于缩放是非常简单的,并且使用矩阵乘法很容易实现,因此我将带大家看一下这个例子。
使用矩阵进行缩放
首先,需要知道一个物体现有的宽度,高度和深度 —— 换句话讲,它是三个轴上每个轴分量的大小。当然可以建立一个3×1的矩阵:
w h d
我们知道 w, h, d 代表宽度(width),高度(height)和深度(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)
这句话用ActionScript大略可以翻译成:
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,现在大家已经有了足够的基础将这些知识转换为代码了。下面,我们对第十五章的RotateXY.as进行重新转换。这个类中有rotateX和rotateY两个方法,用以实现 3D坐标旋转。我们要让它们以矩阵的方式工作。
从rotateX函数开始。它会用到小球的x, y, z坐标,将它们放入 1×3矩阵,然后创建一个给定角度的x旋转矩阵。这个矩阵将使用数组的形式表示。最后使用matrixMultiply函数让两个矩阵相乘,当然还需要创建这个函数!相乘后的矩阵还要用另一个数组进行保存,因为我们需要将这些数值再存回小球的x, y, z坐标中。下面是新版的方法:
private function rotateX(ball:Ball3D, angleX:Number):void {
var position:Array = [ball.xpos, ball.ypos, ball.zpos];
var sin:Number = Math.sin(angleX);
var cos:Number = Math.cos(angleX);
var xRotMatrix:Array = new Array();
xRotMatrix[0] = [1, 0, 0];
xRotMatrix[1] = [0, cos, sin];
xRotMatrix[2] = [0, -sin, cos];
var result:Array = matrixMultiply(position, xRotMatrix);
ball.xpos = result[0];
ball.ypos = result[1];
ball.zpos = result[2];
}
下面是矩阵乘法的函数:
private function matrixMultiply(matrixA:Array, matrixB:Array):Array {
var result:Array = new Array();
result[0] = matrixA[0] * matrixB[0][0] +
matrixA[1] * matrixB[1][0] +
matrixA[2] * matrixB[2][0];
result[1] = matrixA[0] * matrixB[0][1] +
matrixA[1] * matrixB[1][1] +
matrixA[2] * matrixB[2][1];
result[2] = matrixA[0] * matrixB[0][2] +
matrixA[1] * matrixB[1][2] +
matrixA[2] * matrixB[2][2];
return result;
}
现在,这个矩阵乘法的函数是手工写出的一个1×3和3×3矩阵的乘法,这就是我们后面用在每个例子中的函数,
大家也可以使用for循环创建出更为动态的可处理任何大小的矩阵函数,但是现在我要让代码保持简洁。
最后创建rotateY函数。如果你了解 rotateX函数,那么这个函数应该非常显而易见了。只需要创建一个y旋转矩阵来代替x旋转矩阵即可。
private function rotateY(ball:Ball3D, angleY:Number):void {
var position:Array = [ball.xpos, ball.ypos, ball.zpos];
var sin:Number = Math.sin(angleY);
var cos:Number = Math.cos(angleY);
var yRotMatrix:Array = new Array();
yRotMatrix[0] = [ cos, 0, sin];
yRotMatrix[1] = [ 0, 1, 0];
yRotMatrix[2] = [-sin, 0, cos];
var result:Array = matrixMultiply(position, yRotMatrix);
ball.xpos = result[0];
ball.ypos = result[1];
ball.zpos = result[2];
}
就是这样。大家也可以创建一个rotateZ函数,由于我们的例子中实际上不需要用到它,所以我将它作为练习留给大家完成。
现在,运行一下RotateXY.as,与第十五章的版本相比,它们看上去实际是一样的。在AS 2中,我发现非矩阵版本的运行得更为流畅一些。原因是我们为3D旋转和缩放执行了非常大量的数学运算。当我们使用矩阵数学进行计算时,会产生额外的计算。在进行矩阵乘法时,我们实际是做了四次乘以零的操作,并将这四个结果与其它数值相加。这八次数学运算实际上没有任何作用。将这些操作乘以 50 个对象,每帧旋转二次,每帧就多做了 800 次额外计算!这两个版本在AS 3中的运行时看不出任何的不同,这就是Flash CS3 与 AS 3 强大的证明。但是,当加入的物体越来越多时,我们就要为这些巨大的计算量付出代价。我给大家的这些代码都非常基本的。你也许可以使它更加优化一些,让性能得到提升。
即使在3D中不使用矩阵,我们仍可以发现它们在其它方面的用途,我将在下面一节进行介绍。在3D中使用矩阵是一个很好的引子,因为这样可以让大家看到它们是如何与已知公式相关联的。同样,矩阵在其它语言的3D 制作中应用得非常之广泛,而且比我们现在的ActionScript. 更为有效。在这些语言中,只需付出一点点 CPU 就可以得到矩阵所带来的组织良好的代码。如果大家试图在Flash 以外的其它软件中进行 3D 动画编程,那么就一定要使用到矩阵。还是那句话,谁知道 Flash 播放器几年后会成为什么样?终会有一天,所有的这些技术都能与 Flash 完美地结合。
Matrix类
刚刚提到,学习矩阵的一个很好的理由是它被用在许多ActionScript类的内核中。事实上,我们有一个内置矩阵类。浏览一下Flash 帮助文档中的flash.geom.Matrix类,就会发现那里写得非常清楚详细。如果本章前面内容您都能理解,那么要掌握这些材料就一定没问题。文档写得非常好,我就不再浪费空间将这些内容重复一遍了,但是我会给大家一个快速的总结并举出两个例子。
矩阵主要用于对显示对象的转换(旋转,缩放和平移)。现在,任何一个显示对象(Sprite,影片剪辑,文本类等)都有名为transform. (转换)的属性。这是flash.geom.Transform类的一个实例,它还包含有另一个名为matrix的属性。如果我们创建一个Matrix类的实例,并把它赋给显示对象的transform.matrix 属性,那么它将会改变这个对象的形状、大小或位置。我们马上会看到一些具体的例子。
基本来说 Matrix类的矩阵是一个3×3的矩阵,形式如下:
a b tx
c d ty
u v w
其中 u, v, w 内部自动被设置为0, 0, 1。而且它们是不可改变的,因此不需要管它们。(更为具体的解释请参见帮助文档)我们使用下述语法来创建一个新的Matrix:
import flash.geom.Matrix;
var myMatrix:Matrix = new Matrix(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 代表我们想要旋转的某个角度的正弦和余弦值(弧度制)。让我们试验一下这个例子。
这里可见 MatrixRotate.as,这个类中用红色正方形创建了一个简单影片。然后设置一个enterFrame. 处理函数,所有的动作都加在其中:
package {
import flash.display.Sprite;
import flash.events.Event;
import flash.geom.Matrix;
public class MatrixRotate extends Sprite {
private var angle:Number = 0;
private var box:Sprite;
public function MatrixRotate() {
init();
}
private function init():void {
box = new Sprite();
box.graphics.beginFill(0xff0000);
box.graphics.drawRect(-50, -50, 100, 100);
box.graphics.endFill();
addChild(box);
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
private function onEnterFrame(event:Event):void {
angle += .05;
var cos:Number = Math.cos(angle);
var sin:Number = Math.sin(angle);
box.transform.matrix = new Matrix(cos, sin,
-sin, cos,
stage.stageWidth / 2,
stage.stageHeight / 2);
}
}
}
这里有一个angle 变量,每帧都会增加。代码求出了角度的正弦和余弦值并将它们赋给新的矩阵对象,以这种方式指定 rotation。我同时还设置了平移,根据舞台的宽度和高度把影片放置到中心。新的矩阵被赋给了影片的transform.matrix 属性。测试该影片就得到了一个旋转的正方形。
现在,也许有人会问,你什么不改变影片的rotation 属性。在一个简单的例子中使用rotation 没有问题,这是一个更为简单的解决方法。但是,也许在一些处理多个角度、弧度、正弦、余弦的例子中,相比将一切转换回角度制并改变 rotation 值而言,像这样的矩阵赋值确实要简单很多。
再给大家一些实际的说明,让我们试一下倾斜。倾斜(Skewing)意思是将物体在一个轴上进行拉伸以便使一个部分走一条路,另一个部分走另一条路。斜体字就是一个倾斜的例子。字母顶部的部分向右倾斜,而底部的部分向左倾斜。这是Flash中一个众所周知的一个难点,但是使用Matrix类将会惊人地简单。如同我前面所说,设置矩阵的a和d 为1。属性 b是y轴倾斜的值,属性 c 控制 x轴的倾斜值。让我们先来试一下x 倾斜。在SkewX.as中,我几乎使用了与前一个例子完全相同的设置,只不过改变了 onEnterFrame. 方法中矩阵的创建。
private function onEnterFrame(event:Event):void {
var skewX:Number = (mouseX - stage.stageWidth / 2) * .01;
box.transform.matrix = new Matrix(1, 0,
skewX, 1,
stage.stageWidth / 2,
stage.stageHeight / 2);
}
这里相对于鼠标的x坐标创建了一个skewX 变量,以舞台的中心为偏移量。然后将它乘以 .01让倾斜的值处于可控范围,并将此值赋给矩阵。
测试影片后,我们将看到如何让一个完整的影片进行倾斜,如图 18-2 所示。有了 Matrix类一切都变得可能,如果你知道有谁在试图做这样的事,那么就把上述代码拿给他们看,等着看他们开始流口水吧!如果您亲自测试了这段代码,那么肯定已经知道我的意思了。
图18-2 影片在x轴上的倾斜
在SkewXY中,我在y轴上做了同样的事情:
private function onEnterFrame(event:Event):void {
var skewX:Number = (mouseX - stage.stageWidth / 2) * .01;
var skewY:Number = (mouseY - stage.stageHeight / 2) * .01;
box.transform.matrix = new Matrix(1, skewY,
skewX, 1,
stage.stageWidth / 2,
stage.stageHeight / 2);
}
从图18-3中可以看到影片在两个轴上的倾斜
图18-3 影片在两个轴上的倾斜
如此简单就能实现这样的效果的确让人惊喜。如果您不确定这种效果能用在哪里,那么我告诉您,倾斜效果在伪3D效果中使用得非常频繁。当我们在上个例子中移动鼠标时,如果这个图形正在倾斜并旋转,那么大家已经可以看到了它是如何显示出透视来的。这不是特别精确的3D,但是它可以用在一些非常棒的效果中。在网上有一些这方面的教程,告诉我们如何使用倾斜实现这种伪3D效果。Matrix的使用也许会将代码缩短一半。
大家一定要为Matrix类查一查帮助文档,因为这里还有其它好多好东西。要知道上述这些并不是AS 3 唯一使用矩阵的地方。大家还应该看看ColorMatrixFilter,ConvolutionFilter,这些不同的制图 API 填充和渐变的方法,以及 flash.geom.Transform类。所以说矩阵的应用非常广泛!
篇7:Flash基础理论课 第十七章 背面剔除与3D灯光Ⅱ
返回“Flash基础理论课 - 目录”
3D灯光
刚刚这个例子近乎可以让我们的渲染达到完美的效果,但是它似乎还缺少点儿什么,有些单调。OK,OK,大家看到标题就已经知道了,下面就让我们加入3D的灯光效果吧。
同背面剔除一样,3D灯光的细节也是相当复杂并且需要数学运算的。我实在没有太多的空间讨论每个漂亮的细节,但是通过快速的网络搜索大家可以获得非常更多的相关资料,也许这些资料多得我们一生也看不完。在这里,我给大家的都是一些基础的需要用到的函数。
首先,需要一个光源。一个最简单的光源只有两个属性:位置和亮度(brightness)。在更加复杂的 3D 系统中,它也能够指向某个方向,并且还带有颜色,衰减率(falloff rate),圆锥区域等等。但是这些都超出了本例的范围。
让我们从制作一个 Light灯光类开始。它会持有我们刚说的那两个属性 —— 位置和亮度。
package {
public class Light {
public var x:Number;
public var y:Number;
public var z:Number;
private var _brightness:Number;
public function Light(x:Number = -100,
y:Number = -100,
z:Number = -100,
brightness:Number = 1) {
this.x = x;
this.y = y;
this.z = z;
this.brightness = brightness;
}
public function set brightness(b:Number):void {
_brightness = Math.max(b, 0);
_brightness = Math.min(_brightness, 1);
}
public function get brightness():Number {
return _brightness;
}
}
}
现在可以在主类的 init 方法中创建一个新的默认灯光:
var light:Light = new Light();
或者可以创建一个指定位置和区域的灯光:
var light:Light = new Light(100, 200, 300, .5);
这里有两个重要的地方需要注意。一个是位置,仅用于计算灯光的角度。灯光的亮度不会因为距离而衰减。因此改变 x, y, z 到 –1,000,000 或 -1 对于照射在物体上的灯光的亮度是没有区别的。
只有 brightness 属性才会改变灯光的特性。我们当然可以加入一个函数用以判断灯光与物体间的距离来计算灯光的亮度值(brightness)。不会很难,现在已经介绍得差不多了,因此把这个函数留给大家去做。
brightness 必需是 0.0 到 1.0 之间的数。如果出了这个范围,会带来一些奇怪的结果。就是这个原因,我创建了一个私有属性 _brightness,并允许通过公共的 getter和setter 访问 brightness。这样做,允许我们传入的数值得到有效性的验证,确保这个数在有效范围内。
一个理想的类不应该出现公有的属性,即使这些属性不需要验证,也只有私有属性通过 getter和setter 函数才能访问。这里我抄了近路,为的是让代码简洁并突出动画编程的原则。但是在本例中,额外添加的这一步是有必要的。
下面,光源要做的就是根据灯光照射在多边形上的角度来改变三角形颜色的亮度值。因此如果一个多边形直接面对灯光,它就会显示出全部的颜色值。当离开灯光时,就会变得越来越暗。最终,当它完全离开光源时,它将完全变为阴影或黑色。
由于 Triangle 类的成员知道自己的颜色是什么,并知道如何绘制自己,似乎每个三角形只需访问这个 light 就可以实现自己 draw 函数。因此,让我们给所有三角形一个 light 属性。我还要超个近路设置它们为公有属性:
public var light:Light;
然后在主类中,创建这些三角形后,只需要循环它们把灯光的引用赋值给每个三角形:
var light:Light = new Light();
for(i = 0; i < triangles.length; i++) {
triangles[i].light = light;
}
或者,我们也可以让 light 作为 Triangle 构造函数中的一个附加的参数,让每个三角形都有一个光源。我将这个方法留给大家去选择。
现在,Triangle 需要一个关于其灯光颜色、角度、亮度的函数,并返回一个调整后的颜色值。以下是这个函数:
function getAdjustedColor():uint {
var red:Number = color >>16;
var green:Number = color >>8 & 0xff;
var blue:Number = color & 0xff;
var lightFactor:Number = getLightFactor();
red *= lightFactor;
green *= lightFactor;
blue *= lightFactor;
return red << 16 | green << 8 | blue;
}
这个函数首先将三角形的基本颜色分为了 red, green, blue 三个成分(见第四章)。然后调用另一个方法 getLightFactor,稍后会看到这个函数。现在,只需要知道它返回的是 0.0 到 1.0 之间的一个数,表示该颜色需要改变的大小,1.0 表示全部亮度,0.0 表示为全黑色。
然后将每个颜色成分乘以这个滤光系数(light factor),最后再将它们组合为一个 24 位的颜色值,并且将它作为调整后的颜色返回。它将成为灯光照射下三角形的颜色。
现在,如何得到这个 lightFactor 呢?让我们看一下:
private function getLightFactor():Number {
var ab:Object = new Object();
ab.x = pointA.x - pointB.x;
ab.y = pointA.y - pointB.y;
ab.z = pointA.z - pointB.z;
var bc:Object = new Object();
bc.x = pointB.x - pointC.x;
bc.y = pointB.y - pointC.y;
bc.z = pointB.z - pointC.z;
var norm:Object = new Object();
norm.x = (ab.y * bc.z) - (ab.z * bc.y);
norm.y = -((ab.x * bc.z) - (ab.z * bc.x));
norm.z = (ab.x * bc.y) - (ab.y * bc.x);
var dotProd:Number = norm.x * light.x +
norm.y * light.y +
norm.z * light.z;
var normMag:Number = Math.sqrt(norm.x * norm.x +
norm.y * norm.y +
norm.z * norm.z);
var lightMag:Number = Math.sqrt(light.x * light.x +
light.y * light.y +
light.z * light.z);
return Math.acos(dotProd / normMag * lightMag) / Math.PI * light.brightness;
}
哇,好大一个函数不是吗?要想完全理解它,就一定要对高等向量学有较深的掌握,但是我也会试将基础的地方解释一下。
首先,我们需要找到三角形的法线(normal)。它是一个向量,是三角形平面上的一条垂线,如图 17-5 所示。想象一下,我们拿着一块木制的三角板,然后从背后钉入一根钉子,它会从正面穿出。这根钉子就代表三角形平面的法线。如果您学过 3D 渲染和灯光的话,一定看过各种关于法线的资料。
图17-5 法线是到达三角形表面的一条垂线
我们可以通过该平面的两个向量计算出它们的外积(cross product)从而求出这条法线。两个向量的积是一条垂直于这两条向量的新向量。我们将使用的这两条向量是点 A和B,点 B和C 之间的连线。每个向量都用有带有 x, y, z的 Object 持有。
var ab:Object = new Object();
ab.x = pointA.x - pointB.x;
ab.y = pointA.y - pointB.y;
ab.z = pointA.z - pointB.z;
var bc:Object = new Object();
bc.x = pointB.x - pointC.x;
bc.y = pointB.y - pointC.y;
bc.z = pointB.z - pointC.z;
然后计算法线,即另一个向量。求该对象的模(norm)。下面的代码用于计算向量ab和bc的外积:
var norm:Object = new Object();
norm.x = (ab.y * bc.z) - (ab.z * bc.y);
norm.y = -((ab.x * bc.z) - (ab.z * bc.x));
norm.z = (ab.x * bc.y) - (ab.y * bc.x);
我没有太多的篇幅来介绍这种计算方法的细节,这是计算向量外积的标准公式。如果您对它的推导感兴趣,可以随便找一本线性代数的正规参考书查一查。
现在我们需要知道这条法线与灯光的角度。向量数学的另一个好东西叫做内积(dot product),它与外积不同。我们有了法线的向量和灯光的向量。下面计算点积:
var dotProd:Number = norm.x * light.x + norm.y * light.y + norm.z * light.z;
我们看到,内积要比外积简单一些!
OK,都差不多了!接下来,计算法线的量值,以及灯光的量值,大家应该还认识这个 3D 版的勾股定理吧:
var normMag:Number = Math.sqrt(norm.x * norm.x + norm.y * norm.y + norm.z * norm.z);
var lightMag:Number = Math.sqrt(light.x * light.x + light.y * light.y + light.z * light.z);
请注意,当一个三角形被渲染时,变量lightMag 每次都要进行计算,这样就允许灯光是移动的。如果知道光源是固定的,我们可以在代码的一开始就加入这个变量,只需在创建灯光或为三角形赋值时进行一次计算。或者可以为 Light 类添加 lightMag 属性,让它可以在每次 x, y, z 属性发生变化时被计算。看,我已经给大家留出了各种发挥的空间!
最后,将前面计算出的这些数放入一个具有魔力公式中:
return (Math.acos(dotProd / (normMag * lightMag)) / Math.PI) * light.brightness;
其中 dotProd 是一个分量,而 normMag * lightMag 是另一个分量。两者相除得出一个比率。回忆一下第三章,一个角度的余弦给了我们一个比率,而一个比率的反余弦给了我们一个角度。这就是灯光照射在多边形表面上的角度。它的范围在 0 到 Math.PI 个弧度之间(0 到 180 度),也就是说灯光完全照射在物体前面上或完全照射在物体背面。
用这个数除以Math.PI得出一个百分数,再用它乘以brightness的百分比就得出了最终用于改变底色的滤光系数。
OK,所有这些仅仅给出了多边形表面颜色!此刻,在现有的代码中实现它就非常简单了。我们在 draw 方法中使用它。应该像这样直接使用这个调整后的颜色:
g.beginFill(getAdjustedColor());
为了把上述内容综合起来,以下是全部最终的 Triangle.as和ExtrudedA.as 代码,列出了我们本章所有发生变化的部分:
首先是 Triangle:
package {
import flash.display.Graphics;
public class Triangle {
private var pointA:Point3D;
private var pointB:Point3D;
private var pointC:Point3D;
private var color:uint;
public var light:Light;
public function Triangle(a:Point3D, b:Point3D,
c:Point3D, color:uint) {
pointA = a;
pointB = b;
pointC = c;
this.color = color;
}
public function draw(g:Graphics):void {
if (isBackFace()) {
return;
}
g.beginFill(getAdjustedColor());
g.moveTo(pointA.screenX, pointA.screenY);
g.lineTo(pointB.screenX, pointB.screenY);
g.lineTo(pointC.screenX, pointC.screenY);
g.lineTo(pointA.screenX, pointA.screenY);
g.endFill();
}
private function getAdjustedColor():uint {
var red:Number = color >>16;
var green:Number = color >>8 & 0xff;
var blue:Number =color & 0xff;
var lightFactor:Number = getLightFactor();
red *= lightFactor;
green *= lightFactor;
blue *= lightFactor;
return red << 16 | green << 8 | blue;
}
private function getLightFactor():Number {
var ab:Object = new Object();
ab.x = pointA.x - pointB.x;
ab.y = pointA.y - pointB.y;
ab.z = pointA.z - pointB.z;
var bc:Object = new Object();
bc.x = pointB.x - pointC.x;
bc.y = pointB.y - pointC.y;
bc.z = pointB.z - pointC.z;
var norm:Object = new Object();
norm.x = (ab.y * bc.z) - (ab.z * bc.y);
norm.y = -((ab.x * bc.z) - (ab.z * bc.x));
norm.z = (ab.x * bc.y) - (ab.y * bc.x);
var dotProd:Number = norm.x * light.x +
norm.y * light.y +
norm.z * light.z;
var normMag:Number = Math.sqrt(norm.x * norm.x +
norm.y * norm.y +
norm.z * norm.z);
var lightMag:Number = Math.sqrt(light.x * light.x +
light.y * light.y +
light.z * light.z);
return Math.acos(dotProd / normMag * lightMag) / Math.PI * light.brightness;
}
private function isBackFace():Boolean {
// 见 www.jurjans.lv/flash/shape.html
var cax:Number = pointC.screenX - pointA.screenX;
var cay:Number = pointC.screenY - pointA.screenY;
var bcx:Number = pointB.screenX - pointC.screenX;
var bcy:Number = pointB.screenY - pointC.screenY;
return cax * bcy >cay * bcx;
}
public function get depth():Number {
var zpos:Number = Math.min(pointA.z, pointB.z);
zpos = Math.min(zpos, pointC.z);
return zpos;
}
}
}
然后是 ExtrudedA:
package {
import flash.display.Sprite;
import flash.events.Event;
public class ExtrudedA extends Sprite {
private var points:Array;
private var triangles:Array;
private var fl:Number = 250;
private var vpX:Number = stage.stageWidth / 2;
private var vpY:Number = stage.stageHeight / 2;
public function ExtrudedA() {
init();
}
private function init():void {
points = new Array();
points[0] = new Point3D( -50, -250, -50);
points[1] = new Point3D( 50, -250, -50);
points[2] = new Point3D( 200, 250, -50);
points[3] = new Point3D( 100, 250, -50);
points[4] = new Point3D( 50, 100, -50);
points[5] = new Point3D( -50, 100, -50);
points[6] = new Point3D(-100, 250, -50);
points[7] = new Point3D(-200, 250, -50);
points[8] = new Point3D( 0, -150, -50);
points[9] = new Point3D( 50, 0, -50);
points[10] = new Point3D( -50, 0, -50);
points[11] = new Point3D( -50, -250, 50);
points[12] = new Point3D( 50, -250, 50);
points[13] = new Point3D( 200, 250, 50);
points[14] = new Point3D( 100, 250, 50);
points[15] = new Point3D( 50, 100, 50);
points[16] = new Point3D( -50, 100, 50);
points[17] = new Point3D(-100, 250, 50);
points[18] = new Point3D(-200, 250, 50);
points[19] = new Point3D( 0, -150, 50);
points[20] = new Point3D( 50, 0, 50);
points[21] = new Point3D( -50, 0, 50);
for (var i:uint = 0; i < points.length; i++) {
points[i].setVanishingPoint(vpX, vpY);
points[i].setCenter(0, 0, 200);
}
triangles = new Array();
triangles[0] =new Triangle(points[0], points[1],
points[8], 0xcccccc);
triangles[1] =new Triangle(points[1], points[9],
points[8], 0xcccccc);
triangles[2] =new Triangle(points[1], points[2],
points[9], 0xcccccc);
triangles[3] =new Triangle(points[2], points[4],
points[9], 0xcccccc);
triangles[4] =new Triangle(points[2], points[3],
points[4], 0xcccccc);
triangles[5] =new Triangle(points[4], points[5],
points[9], 0xcccccc);
triangles[6] =new Triangle(points[9], points[5],
points[10], 0xcccccc);
triangles[7] =new Triangle(points[5], points[6],
points[7], 0xcccccc);
triangles[8] =new Triangle(points[5], points[7],
points[10], 0xcccccc);
triangles[9] =new Triangle(points[0], points[10],
points[7], 0xcccccc);
triangles[10] =new Triangle(points[0], points[8],
points[10], 0xcccccc);
triangles[11] =new Triangle(points[11], points[19],
points[12], 0xcccccc);
triangles[12] =new Triangle(points[12], points[19],
points[20], 0xcccccc);
triangles[13] =new Triangle(points[12], points[20],
points[13], 0xcccccc);
triangles[14] =new Triangle(points[13], points[20],
points[15], 0xcccccc);
triangles[15] =new Triangle(points[13], points[15],
points[14], 0xcccccc);
triangles[16] =new Triangle(points[15], points[20],
points[16], 0xcccccc);
triangles[17] =new Triangle(points[20], points[21],
points[16], 0xcccccc);
triangles[18] =new Triangle(points[16], points[18],
points[17], 0xcccccc);
triangles[19] =new Triangle(points[16], points[21],
points[18], 0xcccccc);
triangles[20] =new Triangle(points[11], points[18],
points[21], 0xcccccc);
triangles[21] =new Triangle(points[11], points[21],
points[19], 0xcccccc);
triangles[22] =new Triangle(points[0], points[11],
points[1], 0xcccccc);
triangles[23] =new Triangle(points[11], points[12],
points[1], 0xcccccc);
triangles[24] =new Triangle(points[1], points[12],
points[2], 0xcccccc);
triangles[25] =new Triangle(points[12], points[13],
points[2], 0xcccccc);
triangles[26] =new Triangle(points[3], points[2],
points[14], 0xcccccc);
triangles[27] =new Triangle(points[2], points[13],
points[14], 0xcccccc);
triangles[28] =new Triangle(points[4], points[3],
points[15], 0xcccccc);
triangles[29] =new Triangle(points[3], points[14],
points[15], 0xcccccc);
triangles[30] =new Triangle(points[5], points[4],
points[16], 0xcccccc);
triangles[31] =new Triangle(points[4], points[15],
points[16], 0xcccccc);
triangles[32] =new Triangle(points[6], points[5],
points[17], 0xcccccc);
triangles[33] =new Triangle(points[5], points[16],
points[17], 0xcccccc);
triangles[34] =new Triangle(points[7], points[6],
points[18], 0xcccccc);
triangles[35] =new Triangle(points[6], points[17],
points[18], 0xcccccc);
triangles[36] =new Triangle(points[0], points[7],
points[11], 0xcccccc);
triangles[37] =new Triangle(points[7], points[18],
points[11], 0xcccccc);
triangles[38] =new Triangle(points[8], points[9],
points[19], 0xcccccc);
triangles[39] =new Triangle(points[9], points[20],
points[19], 0xcccccc);
triangles[40] =new Triangle(points[9], points[10],
points[20], 0xcccccc);
triangles[41] =new Triangle(points[10], points[21],
points[20], 0xcccccc);
triangles[42] =new Triangle(points[10], points[8],
points[21], 0xcccccc);
triangles[43] =new Triangle(points[8], points[19],
points[21], 0xcccccc);
var light:Light = new Light();
for (i = 0; i < triangles.length; i++) {
triangles[i].light = light;
}
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
private function onEnterFrame(event:Event):void {
var angleX:Number = (mouseY - vpY) * .001;
var angleY:Number = (mouseX - vpX) * .001;
for (var i:uint = 0; i < points.length; i++) {
var point:Point3D = points[i];
point.rotateX(angleX);
point.rotateY(angleY);
}
triangles.sortOn(“depth”, Array.DESCENDING | Array.NUMERIC);
graphics.clear();
for (i = 0; i < triangles.length; i++) {
triangles[i].draw(graphics);
}
}
}
}
我们看到,在文档类中只有两个次要的变化,
主要的工作都集中在 Triangle 中。同时,我还让所有的三角形使用相同的颜色,我认为这样做可以更好地观察灯光效果(见图 17-6)。
图17-6 带有背面剔除,深度排序及3D灯光的三维立体模型
★ 缓考申请书
★ 感 动
★ 动词语
Flash基础理论课 第八章 缓动与弹性运动Ⅰ(共7篇)




