转 第十三章 正向运动学:行走(1)(as3.0)

 前面章节介绍的都是 ActionScript 交互动画的基础,也可以说是一些高级“基础”。
现在开始,我们进入另一条有趣的技术之路,运动学。
    到底什么是运动学呢?我所找到的一些资料看起来都有些让人望而却步,  这是一项基于
高级 3D 动画编程的技术。上网搜索一下,会发现其涉及到的方程中到处都是些陌生符号,
这也成为了我们学习的最大障碍,似乎前面所学的内容都像是很基础的算法。  首先,我要说,
运动学并没有那么可怕。前面章节中只介绍了我们所需的一些基本知识,  现在就要将它们加
以组合。
    运动学研究物体的运动,但不考虑施加在物体上的质量或力,基本上是数学的一个分支。
所以,无非就是速度,方向,速度向量等等。听起来不难哈?虽然这是个非常简单的定义,
但是蕴藏在里面的内容是比较复杂的,但是要完成我们的目标已经足够了。
    当人们在计算机科学,图形及游戏等领域谈论运动学的时候, 无非就是在讨论运动学的
两个特殊的分支:正向运动学和反向运动学。让我们就从这里开始吧。

正向和反向运动学介绍
    总体而言,正向和反向运动学系统都是由相互连接的零部件构成,  如一串链条或一串由
关节相连的手臂。它们要负责整个系统的运动,以及某个零件相对于其它零件和整个系统的
运动。
    通常一个运动学系统都有两个末端:固定端(base)和自由端(free)。带有关节的手
臂通常有一端被固定住,而另一端则用于伸出去抓东西。一串链条也许有一两个末端都连在
某个东西上,或者什么都不连。
    正向运动学(Forward kinematics,缩写:FK)中的运动是以系统的固定端为起始,在
自由端进行运动。反向运动学(Inverse kinematics,缩写:IK)则是向反的:运动以自由
端为起始,回退到固定端,如果有的话。
    通常情况下,下肢在行走时都看作是正向运动学。大腿的移动带动小腿的运动,小腿的
移动带动脚的运动,最终让脚产生运动。脚的运动不会决定其它部分的运动,它所带动的就
是它本身,其位置根据下肢的位置来确定。
    一个反向运动学的例子,就是用手拉某人。这里,力作用于自由端—— 手 ——控制手,
前臂,大臂,以致整个身体的位置和运动。
    说得更细一点,例如反向运动学是一支手伸出去拿东西,  而手就传动了这个系统。当然,
也可以说,大臂和前臂也是运动的,它们控制了手的位置。没错,不过最直接的目的是把手
放在某个特定的位置。这就是传动力。它不是一种实际的力,而是一种意图。前臂和大臂只
是根据结构的需要通过排列它们的位置来设置手的位置。
    我们会在下一章通过具体的例子弄清二者间的差别。  不过现在,要记住拖动和伸臂是一
般的反向运动学,而一个重复的循环运动,如行走,则是最常见的正向运动学,也就是本章
的课题。

正向运动学编程准备
     两种运动学编程都有如下基本要素:
■ 系统部件。我们称为关节(Segment)。
■ 各关节的位置。
■ 各关节的旋转。
   例子中的每个关节都是一个长方形,就像前臂、大臂,或大腿的一部分。当然,最末端
的关节可以是任意的形状,比如手,脚,钳子,刺或是入侵者的绿色激光炮。
    每个关节的都会有一个末端作为枢轴,围绕它可以进行旋转。如果这个关节还有其它子
关节的话,子关节还会以它们的另一末端作为旋转的枢轴。就像大臂绕着肩膀旋转,小臂绕
着肘部旋转,手绕着手腕旋转一样。
    当然,在很多现实的系统中,这种绕轴旋转可以有多个方向。想一想我们有多少种方法
可以让手绕着手腕旋转。到本书的最后,大家也许会在 Flash 中亲自进行一下尝试。但是,
现在,我们这个系统完全是二维的。

单关节运动
      让我们从单个关节的运动入手。首先,需要一个像关节一样的物件。设想一下,它应该
是一个继承自 Sprite 的自定义类。因为 sprite 影片可以进行绘图,设定位置,旋转,加
入显示列表。以下是这个类 Segment.as:
package {
  import flash.display.Sprite;
  import flash.geom.Point;
  public class Segment extends Sprite {
   private var color:uint;
   private var segmentWidth:Number;
   private var segmentHeight:Number;
   public var vx:Number = 0;
   public var vy:Number = 0;
   public function Segment(segmentWidth:Number,
   segmentHeight:Number,
   color:uint = 0xffffff) {
     this.segmentWidth = segmentWidth;
     this.segmentHeight = segmentHeight;
     this.color = color;
     init();
   }
   public function init():void {
     // 绘制关节
     graphics.lineStyle(0);
     graphics.beginFill(color);
     graphics.drawRoundRect(-segmentHeight / 2,
     -segmentHeight / 2,
     segmentWidth + segmentHeight,
     segmentHeight,
     segmentHeight,
     segmentHeight);
  graphics.endFill();
  // 绘制两个“枢轴”
  graphics.drawCircle(0, 0, 2);
  graphics.drawCircle(segmentWidth, 0, 2);
}
public function getPin():Point {
  var angle:Number = rotation * Math.PI / 180;
     var xPos:Number = x + Math.cos(angle) * segmentWidth;
     var yPos:Number = y + Math.sin(angle) * segmentWidth;
     return new Point(xPos,yPos);
   }
  }
}
      用宽度、高度、颜色,以及绘制一个圆角矩形定义一个关节。再绘制两个小圆,一个放
在关节的注册点(0,0)位置上,另一个则放在相对的另一个末端,这两个“枢轴”用于关
节之间的相互连接。(大家也许注意到了两个公共变量 vx 和 vy。稍后在本章的“处理反
作用力”一节会做更多的介绍)下面一段代码使用不同的宽度和高度创建了若干个关节
(segment),给大家一些创建 Segment 类实例的启发:
var segment:Segment = new Segment(100, 20);
addChild(segment);
segment.x = 100;
segment.y = 50;
var segment1:Segment = new Segment(200, 10);
addChild(segment1);
segment1.x = 100;

segment1.y = 80;
var segment2:Segment = new Segment(80, 40);
addChild(segment2);
segment2.x = 100;
segment2.y =120;
这段代码的执行结果如图 13-1 所示。

转 <wbr>第十三章 <wbr>正向运动学:行走(1)(as3.0)

图 13-1 一些关节示例
    图 13-1 中一个要点是关节的宽度可以决定两个枢轴间的距离,关节实际的宽度超出了
二者的范围。我们可以看到每个关节都放在了 x 轴为 100 的位置上。虽然它们的左侧的边
没有排列整齐,但所有左侧的枢轴却排列得很整齐。当我们旋转关节时,则会绕着左侧的枢
轴进行旋转。
    我们同样注意了 Segment 类的代码有一个公共的 getPin() 方法,返回一个
flash.geom.Point 的实例。它将返回右侧枢轴的 x,y 坐标。显然,它会随着关节的旋转而
改变,所以我们使用一些基本的三角学来计算这个位置。这也就是下一个关节将连接上的位
置——我们会在本章看到下一个关节。
    对于第一个例子程序,SingleSegment.as,已经创建好了… …在舞台上放置单个关节。
同时,我还创建一个滑块类 SimpleSlider.as 加入到了这个项目中。大家可以从本书的下
载页面 www.friendsofed.com 中进行下载,这样我们就可以在任何时候自由地使用这个类
了,它用于在运行时对数值进行调整。对于这个滑块我们可以在构造函数中设置最小值,最
大值,以及当前取值。下面例子中,将最小值设为 -90,最大值设为 90,取值设为 0。这
个类文件结合了上述所有内容:
package {
  import flash.display.Sprite;
  import flash.events.Event;
  public class SingleSegment extends Sprite {
   private var slider:SimpleSlider;
   private var segment:Segment;
   public function SingleSegment() {
     init();
   }
   private function init():void {
     segment = new Segment(100,20);
     addChild(segment);
     segment.x = 100;
     segment.y = 100;
     slider = new SimpleSlider(-90,90,0);
     addChild(slider);
     slider.x = 300;
     slider.y = 20;
     slider.addEventListener(Event.CHANGE,onChange);
   }
   private function onChange(event:Event):void {
     segment.rotation = slider.value;
   }
  }
}



转 <wbr>第十三章 <wbr>正向运动学:行走(1)(as3.0)
图 13-2 动起来了!
    意思是说无论何时滑块(slider)的取值发生了改变,都将调用 onChange 方法,设置
segment 的 rotation 为当前滑块的值。测试一下,运行结果如图 13-2 所示。如果运行没
有问题,我们就完成了正向运动学的第一阶段。

双关节的运动
     现在我们要继续前进。最初的滑块和关节都命名为 slider0 和 segment0,再创建另一
个关节的实例,名为 segment1,还要创建一个滑块实例名为 slider1。新滑块将控制新关
节的运动,新关节的位置将由 segment0 的 getPin() 方法来确定。以下是代码,见
TwoSegments.as:
package {
  import flash.display.Sprite;
  import flash.events.Event;
  public class TwoSegments extends Sprite {
   private var slider0:SimpleSlider;
private var slider1:SimpleSlider;
private var segment0:Segment;
private var segment1:Segment;
public function TwoSegments() {
  init();
}
private function init():void {
  segment0 = new Segment(100,20);
  addChild(segment0);
  segment0.x = 100;
  segment0.y = 100;
  segment1 = new Segment(100,20);
  addChild(segment1);
  segment1.x = segment0.getPin().x;
  segment1.y = segment0.getPin().y;
  slider0 = new SimpleSlider(-90,90,0);
  addChild(slider0);
  slider0.x = 320;
  slider0.y = 20;
  slider0.addEventListener(Event.CHANGE,onChange);
  slider1 = new SimpleSlider(-90,90,0);
  addChild(slider1);
  slider1.x = 340;
  slider1.y = 20;
  slider1.addEventListener(Event.CHANGE,onChange);
}
private function onChange(event:Event):void {
  segment0.rotation = slider0.value;
  segment1.rotation = slider1.value;
  segment1.x = segment0.getPin().x;
     segment1.y = segment0.getPin().y;
   }
  }
}

转 <wbr>第十三章 <wbr>正向运动学:行走(1)(as3.0)

 

图 13-3 双关节正向运动
    快速浏览一下 onChange 方法,我们看到这里用 segment0.getPin() 的返回值来设置
segment1 的位置。与首次创建 segment1 时设置位置的代码相同。
    我们让 slider1 和 slider0 都来调用 onChange 方法。很明显,现在 segment1 的
rotation 要根据 slider1 来确定。
    测试一下,我们看到当旋转 segment0 时,segment1 依然与它的末端相连,如图 13-3
所示。要知道两者间没有实际的物理链接,而都是用数学的方法计算出来的。我们同样可以
使用 segment1 的滑块独立地对它进行旋转。为了好玩一点,改变每个关节的宽度和高度后,
程序仍可以完美地工作。不过,看起来有些奇怪,因为当 segment0 带动 segment1 运动时;
segment0 并没有带动 segment1 旋转。这就像安装了回旋稳定器,将它的方向稳定住了一
样。我不知道您是怎么样的,反正我的前臂没有装回旋稳定器(尽管可能会很酷),因此,
这样的运动看起来不太自然。真正的情况应该是,segment1 的旋转应等于 segment0 的
rotation 加上 slider1 的值。那么 TwoSegment2.as 文档类的函数应该是这样:
private function onChange(event:Event):void {
  segment0.rotation = slider0.value;
  segment1.rotation = segment0.rotation + slider1.value;
  segment1.x = segment0.getPin().x;
  segment1.y = segment0.getPin().y;
}
     现在,看起来像是真正的一条手臂了。当然,如果讨论人类的手臂,就不能像这个肘部
一样向两个方向弯曲。我们只需要改变 slider1 的范围,让最小值等于 -160 让最大值为
0,那么看上去就更加正常了,代码如下:
     slider1 = new SimpleSlider(-160, 0, 0);
     现在应该是重新思考正向运动学的最佳时机了。这个系统的固定端是 segment0 的枢轴
点,自由端是 segment1 的自由端。这时我们可以想象一下手臂。固定端的旋转和位置决定
了 segment1 的位置。 segment1 的旋转和位置决定了其自由端的位置。
                     而                                            自由端在哪无所
谓,它只是来凑凑热闹而已。因此,控制就是从固定端向前运动到自由端。
自动化过程
      这些滑块给了我们控制旋转的权力,但是我们所创建的则是像机械结构中的液压杠杆,
让零件产生运动。如果想实现真正的行走,就需要加入一些自动控制。
      只需要让每个关节平稳地前后摇摆,并让它们保持同步。听起来就像正弦波一样。
      在 Walking1.as 中,我用三角函数代替了滑块。设置一个正弦循环变量(初始为 0)
乘以 90,让数值的变化从 90 到 -90。循环变量是实时累加的,这样就可以实现摆动。接
下来就可以使用计算出的角度变量来控制两个关节了。加入 enterFrame 函数来控制动作,
这样运动就是持续的了。
package {
  import flash.display.Sprite;
  import flash.events.Event;
  public class Walking1 extends Sprite {
   private var segment0:Segment;
   private var segment1:Segment;
   private var cycle:Number = 0;
   public function Walking1() {
     init();
   }
   private function init():void {
     segment0 = new Segment(100,20);
     addChild(segment0);
     segment0.x = 200;
     segment0.y = 200;
     segment1 = new Segment(100,20);
     addChild(segment1);
     segment1.x = segment0.getPin().x;
     segment1.y = segment0.getPin().y;
      addEventListener(Event.ENTER_FRAME,onEnterFrame);
   }
    private function onEnterFrame(event:Event):void {
      cycle += .05;
      var angle:Number = Math.sin(cycle) * 90;
      segment0.rotation = angle;
      segment1.rotation = segment0.rotation + angle;
      segment1.x = segment0.getPin().x;
      segment1.y = segment0.getPin().y;
    }
  }
}

创建自然行走循环
     OK,现在我们已经可以让胳膊运动了。接下来把它变成腿。进行如下的调整:
1.将 segment0 的旋转增加 90 度,将范围减小到从 90 度开始到每个方向 45 度。
2.每个关节都要有不同的角度,因此要声明两个角度 angle0 和 angle1。
3.将 angle1 的范围减少到 45 度, 然后再增加 45 度。这样就使最终的范围变为 0 到 90
度,因此只能向一个方向弯曲,像是真正的膝关节。光这样说不能完全解释清楚,请大家试
着观察加或不加 45 度有何区别,或试着加入其它的数值,直到感觉都合适为止。
     完成的结果见 Walking2.as。下面列出 onEnterFrame 方法,其它地方没有改变:
private function onEnterFrame(event:Event):void {
  cycle += .05;
  var angle0:Number = Math.sin(cycle) * 45 + 90;
  var angle1:Number = Math.sin(cycle) * 45 + 45;
  segment0.rotation = angle0;
  segment1.rotation = segment0.rotation + angle1;
  segment1.x = segment0.getPin().x;
  segment1.y = segment0.getPin().y;
}
     OK,成功,如图 13-4 所示。开始变得像条腿了,至少运动起来像是条腿。

转 <wbr>第十三章 <wbr>正向运动学:行走(1)(as3.0)


图 13-4 循环行走
    看起来还是不像真正在走路。也许它是在漫不经心地踢球,或是在练习芭蕾舞,但就是
不像在走路。这是因为两个关节在同一时刻内向同一方向运动。它们是完全同步的,而真正
的行走过程,并不是这样的。
    关节的同步是因为它们都使用了相同的循环变量来计算各自的角度。要让它们不再同
步,我们应采用 cycle0 和 cycle1 变量,但是可以不做这么大的改变。只需要加入循环的
偏移量求出 angle1 即可,如下:
     var angle1:Number = Math.sin(cycle + offset) * 45 + 45;
     当然我们需要先定义好偏移(offset)的值。那么应该偏移多少呢?我也不知道。调整
到您认为满意为止吧。给大家一些提示:这个数应该在 Math.PI 到 –Math.PI (3.14 到
-3.14)之间。任何大于或小于这个范围的数,只是将这个数进行了重复而已。例如,我用 –
Math.PI / 2,将 angle0 向后推四分之一个周期。当然,-Math.PI / 2 大约为 -1.57,因
此我们可以试试其周边的一些数如 -1.7 或 -1.3,看看效果是好是坏。稍后,我们将放入
一个滑块来动态地进行调整。带有偏移的循环行走代码见 Walking3.as。
     “单腿行走”对我来说太残酷了,让我们加入另一条腿。需要再加入两个关节,名为
segment2 和 segment3。segment2 对象应与 segment0 位置相同,因为它也将是一个顶级
端或固定端,而 segment3 应该用 segment2 的 getPin() 方法来设置位置。
     下面,我将整个代码抽象为一个方法,名为 walk,这样比复制所有的代码让 segment0
和 segment1 运动要好得多:
private function walk(segA:Segment, segB:Segment, cyc:Number):void {
  var angleA:Number = Math.sin(cyc) * 45 + 90;
  var angleB:Number = Math.sin(cyc + offset) * 45 + 45;
  segA.rotation = angleA;
  segB.rotation = segA.rotation + angleB;
  segB.x = segA.getPin().x;
  segB.y = segA.getPin().y;
}
     注意这个函数有三个参数:两个关节,segA 和 segB,以及 cyc 代表 cycle。剩下的
代码都是我们用过的。现在让 segment0 和 segment1 行走,只需要这样调用:
     walk(segment0, segment1, cycle);
     现在,大家知道如何与它打交道了吧,那么就准备好让 segment2 和 segment3 也走起
来吧。onEnterFrame 方法如下:
private function onEnterFrame(event:Event):void {
  walk(segment0, segment1, cycle);
  walk(segment2, segment3, cycle);
  cycle += .05;
}
     如果这样做的话,就会惊奇地发现,第二条腿不见了。问题在于两条腿是完全同步的,
所以看上去像是只有一条腿。因此,要让它们不能同步运动。我们要将第二条腿与第一条腿
的运动周期进行偏移。 这就要改变 cycle 的值。只需要在 cycle 上面加上或减去一些值就
可以了,这样做比加入两个不同的变量要好得多。因此 onEnterFrame 就变成了这样:
private function onEnterFrame(event:Event):void {
  walk(segment0, segment1, cycle);
  walk(segment2, segment3, cycle + Math.PI);
  cycle += .05;
}
     为什么是 Math.PI?完整的回答是,这个值让第二条腿与第一条腿的同步相差了 180
度,因此在第一条腿向前走时,第二条腿正在向后走,反之亦然。简略的回答是,因为它奏
效了!大家可以试试其它的值,如 Math.PI / 2,观察一下这个运动,就像疾速飞跑,而不
是在行走或跑步。不过需要加以留心——说不定某一天会需要它呢!
     按照惯例文件保存在 Walking4.as 中,如图 13-5 所示。基础的关节("thighs"[大腿])
比末端关节("calves"[小腿])要稍大一些。请记住,由于我们使用的方法是动态的,所以
影片的运行与零件的大小无关。在下一个版本中,我们会用滑块使更多的部件变成动态的,
不过我强烈建议大家现在开始手动改变一下代码中变量的值,  观察一下不同的变量值所带来
的不同效果。
转 <wbr>第十三章 <wbr>正向运动学:行走(1)(as3.0)

图 13-5 看!走起来了!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值