转 第十四章 反向运动学: 拖拽与伸展(as3.0)

  第十三章介绍了一些基础的运动学以及正向与反向运动学之间的区别。  前一章我们讲了
正向运动学,本章就要学习与它关系紧密的反向运动学。涉及到的动作就是拖拽与伸展。
    与正向运动学的例子相同,本章的例子也是从独立的关节开始建立系统。  我们从单个关
节开始,然后到多个关节。首先,我会给大家演示最简单的计算角度与位置的方法。只是在
代码中使用基本的三角学进行大概的测算。最后,会给大家简要地介绍使用余弦定理的方法,
这样计算出来的结果更加准确,但会消耗大量的计算——这就是所谓的权衡。
单物体的拖拽与伸展
    前面说过,反向运动学系统可以分为两种不同的类型:拖拽与伸展。
    当系统的自由端向目标点伸展时,系统的另一端——固定端,也许是动不了的,因此如
果目标点位置超出了自由端运动的范围,那么自由端永远也不能到达目标点。举个例子,当
我们试图抓住某个东西时,手指就朝着这个物体移动,  手腕的转动会使我们的手指与目标位
置越来越近,肘部,肩膀和身体其它的部分也都尽可能地伸展。有时,所有这些位置的组合
将会使手指接触到物体;有时也许不行。如果物体是来回运动的,我们的肢体就要做出即时
的反映不断调整位置,为了让手指能够尽可能地够到该物体。反向运动学将会告诉我们,如
何设置所有这些零件的位置,达到最佳的伸展效果。
    另一种反向运动学是在物体被拖拽的时候。这个例子中,  自由端是被一些外部的力所拖
动的。无论何时,系统其余的部分都紧随其后,它们会将自己放置到自然的可能位置上。可
以想象成一个没有知觉死尸(对不起,这是我唯一能想到的)  。抓住他的手然后拽着它走。
    我们施加在对方手上的力,会传到手腕,肘部,肩膀,以及身体的其余部分,它们都沿
着拖拽的方向移动。这个例子中,反向运动学将告诉我们所有的这些零件是如何随着拖拽组
合成正确的位置。
    最好的理解方法就是用例子程序加以说明,每个例子都使用一个关节。我们需要用到
Segment 这个类,因此要保证它在我们工作的工程或类路径中。
单关节伸展
      对于伸展而言,所有关节都要能向目标旋转。目标,如果还没读懂我的意思,就把它想
需要知道两点间 x,y 轴上的距离。然后就可以使用 Math.atan2成鼠标。让关节向目标旋转,

求出该角度的弧度制。将它转换为角度制,就得到了关节的 rotation。代码如下(可见
OneSegment.as) 

package {
  import flash.display.Sprite;
  import flash.events.Event;
  public class OneSegment extends Sprite {
   private var segment0:Segment;
   public function OneSegment() {
     init();
   }
   private function init():void {
     segment0 = new Segment(100, 20);
     addChild(segment0);
     segment0.x = stage.stageWidth / 2;
     segment0.y = stage.stageHeight / 2;
      addEventListener(Event.ENTER_FRAME, onEnterFrame);
    }
    private function onEnterFrame(event:Event):void {
      var dx:Number = mouseX - segment0.x;
      var dy:Number = mouseY - segment0.y;
      var angle:Number = Math.atan2(dy, dx);
      segment0.rotation = angle * 180 / Math.PI;
    }
  }
}
       图 14-1 所示,运行结果。测试一下观察关节是如何跟随鼠标的。即使关节离得很远,
它都像是快要抓住鼠标一样。

转 <wbr>第十四章 <wbr>反向运动学: <wbr>拖拽与伸展(as3.0)

 

图 14-1 单个关节向鼠标伸展

 


单关节拖拽
      现在,我们来试一试拖拽。这里所说的拖拽不是使用 startDrag 和 stopDrag 方法(虽
然你也可以这样做) 。我们要假设关节的第二个枢轴点是与鼠标相连的。
      拖拽的第一部分与伸展相同:让 sprite 影片向着鼠标旋转。然后我们还要多做一步,
将关节移动到可以使第二个枢轴点放到鼠标上的位置。这样一来,就需要知道两个枢轴的每
个轴的位置。我们可以通过关节的 getPin() 方法以及关节实际的 x,y 位置,将它们计算出
来。把这两个距离叫做 w 和 h 吧。最后,从鼠标的当前位置中将 w 和 h 减去,这样就
知道将关节放在哪里了。下面是 OneSegmentDrag.as 中的 onEnterFrame 方法,也是唯一发
生改变的部分:
private function onEnterFrame(event:Event):void {
  var dx:Number = mouseX - segment0.x;
  var dy:Number = mouseY - segment0.y;
  var angle:Number = Math.atan2(dy, dx);
  segment0.rotation = angle * 180 / Math.PI;
  var w:Number = segment0.getPin().x - segment0.x;
  var h:Number = segment0.getPin().y - segment0.y;
  segment0.x = mouseX - w;
  segment0.y = mouseY - h;
}
      我们可以看到这个关节永久地与鼠标相连并旋转,拖拽在鼠标的后面。我们甚至可以把
这个关节推到相反的方向去。

多关节拖拽

      使用反向运动学拖拽一个系统比伸展要简单一些,所以首先介绍拖拽。从两个关节的拖
拽入手。
拖拽两个关节
      继续前面的例子,再创建一个关节,名为 segment1,然后加入显示列表。策略非常简
单。我们已经有了 segment0 拖拽在鼠标上的位置了,只需要再让 segment1 拖拽在
segment0 上即可。首先,简单地复制一些代码,然后改变一些引用。新代码部分加粗表示。
private function onEnterFrame(event:Event):void {
  var dx:Number = mouseX - segment0.x;
  var dy:Number = mouseY - segment0.y;
  var angle:Number = Math.atan2(dy, dx);
  segment0.rotation = angle * 180 / Math.PI;
  var w:Number = segment0.getPin().x - segment0.x;
  var h:Number = segment0.getPin().y - segment0.y;
  segment0.x = mouseX - w;
  segment0.y = mouseY - h;
  dx = segment0.x - segment1.x;
  dy = segment0.y - segment1.y;
  angle = Math.atan2(dy, dx);
  segment1.rotation = angle * 180 / Math.PI;
  w = segment1.getPin().x - segment1.x;
  h = segment1.getPin().y - segment1.y;
  segment1.x = segment0.x - w;
  segment1.y = segment0.y - h;
}
      我们看到新的代码块是如何计算 segment1 到 segment0 的距离,并使用它们计算出
angle 与 rotation 以及 segment1 的位置。不妨测试一下这个例子程序,观察这个非常真实
的双关节系统。
      现在,有了许多复制的代码,这样不太好。如果要加入更多的关节,这个文件会由于这
些相同的重复代码变得越来越长。解决方法是将复制出来的代码单独放到一个名为 drag 的
函数中。这个函数需要知道要被拖拽的关节以及要拖拽到的点的 x,y。然后我们就可以拖拽
segment0 到 mouseX, mouseY,以及 segment1 到 segment0.x, segment0.y。全部代码如下(同
样出现在 TwoSegmentDrag.as 中) :
package {
  import flash.display.Sprite;
  import flash.events.Event;
  public class TwoSegmentDrag extends Sprite {
   private var segment0:Segment;
   private var segment1:Segment;
   public function TwoSegmentDrag() {
     init();
   }
   private function init():void {
     segment0 = new Segment(100, 20);
     addChild(segment0);
     segment1 = new Segment(100, 20);
     addChild(segment1);
      addEventListener(Event.ENTER_FRAME, onEnterFrame);
    }
    private function onEnterFrame(event:Event):void {
      drag(segment0, mouseX, mouseY);
      drag(segment1, segment0.x, segment0.y);
    }
    private function drag(segment:Segment, xpos:Number, ypos:Number):void {
      var dx:Number = xpos - segment.x;
      var dy:Number = ypos - segment.y;
      var angle:Number = Math.atan2(dy, dx);
      segment.rotation = angle * 180 / Math.PI;
      var w:Number = segment.getPin().x - segment.x;
      var h:Number = segment.getPin().y - segment.y;
      segment.x = xpos - w;
      segment.y = ypos - h;
    }
  }
}
拖拽更多的关节
       现在我们可以任意加入多个关节了。假设放入6个关节 ,命名从 segment0 到
segment1,并把它们存入数组。然后使用 for 循环为每个关节调用 drag 函数。可在
MultiSegmentDrag.as 中找到这个例子。代码如下:
package {
  import flash.display.Sprite;
  import flash.events.Event;
  public class MultiSegmentDrag extends Sprite {
   private var segments:Array;
   private var numSegments:uint = 6;
   public function MultiSegmentDrag() {
     init();
   }
   private function init():void {
     segments = new Array();
     for (var i:uint = 0; i < numSegments; i++) {
       var segment:Segment = new Segment(50, 10);
       addChild(segment);
       segments.push(segment);
     }
     addEventListener(Event.ENTER_FRAME, onEnterFrame);
   }
   private function onEnterFrame(event:Event):void {
     drag(segments[0], mouseX, mouseY);
     for (var i:uint = 1; i < numSegments; i++) {
       var segmentA:Segment = segments[i];
       var segmentB:Segment = segments[i - 1];
       drag(segmentA, segmentB.x, segmentB.y);
      }
    }
    private function drag(segment:Segment, xpos:Number, ypos:Number):void {
      var dx:Number = xpos - segment.x;
      var dy:Number = ypos - segment.y;
      var angle:Number = Math.atan2(dy, dx);
      segment.rotation = angle * 180 / Math.PI;
      var w:Number = segment.getPin().x - segment.x;
      var h:Number = segment.getPin().y - segment.y;
      segment.x = xpos - w;
      segment.y = ypos - h;
    }
  }
}
        segmentB 是要拖拽到的目标关节,segmentA 则是下一个关节——正在被拖拽的关节。
只需要把它们作为参数传给 drag 函数即可。运行结果见图 14-2。

转 <wbr>第十四章 <wbr>反向运动学: <wbr>拖拽与伸展(as3.0)

 

图 14-2 多关节拖拽

 


     现在,大家已经有了反向运动学的基础。不太复杂,哈?想要多少个关节就可以加入多
少个,只需要改变 numSegments 变量即可。图 14-3 中可以看到 50 个关节,充分彰显了
这个系统是多么强大。


转 <wbr>第十四章 <wbr>反向运动学: <wbr>拖拽与伸展(as3.0)
图 14-3 拖拽 50 个关节

 


多关节伸展运动
     反向运动学的伸展,从本章的初始示例 OneSegment.as 开始,并在其基础上加以扩展。
这个程序只是将关节向目标旋转,即鼠标的位置。
抓住鼠标
     首先,我们需要确定关节接触到目标时的实际位置。同拖拽的计算位置的方法一样。然
而,本例中我们并不真正移动关节。只需要找出这个位置。那么获得了这个位置后,能做些
什么呢?我们将它作为下一个关节的目标点, 并让下一个关节向它旋转。 在伸展到这个系统
的固定端时, 在回退回去,将每一个零件放到其父级的末端。 图14-4 解释说明了这一过程。
                   
转 <wbr>第十四章 <wbr>反向运动学: <wbr>拖拽与伸展(as3.0)
图 14-4 segment0 向着鼠标旋转。tx,ty 是它应该到达的位置。segment1 向着 tx,ty 旋转
      本章的第一个文件 OneSegment.as 中只有一个关节,segment0 向鼠标抓去。这里,我
们再创建一个关节,名为 segment1,并加入显示列表。接下来要找到目标点,将 segment0
放到目标点上。与拖拽示例中,将关节拖拽到的点是相同的。但是不要移动它,只要保持这
个位置。由此得出这个函数:
private function onEnterFrame(event:Event):void {
  var dx:Number = mouseX - segment0.x;
  var dy:Number = mouseY - segment0.y;
  var angle:Number = Math.atan2(dy, dx);
  segment0.rotation = angle * 180 / Math.PI;
  var w:Number = segment0.getPin().x - segment0.x;
  var h:Number = segment0.getPin().y - segment0.y;
  var tx:Number = mouseX - w;
  var ty:Number = mouseY - h;
}
      把这个点叫做 tx,ty,因为它将是 segment1 旋转的目标。
      接下来,我们就可以进行复制粘贴,调整旋转代码让 segment1 向目标点旋转:
private function onEnterFrame(event:Event):void {
  var dx:Number = mouseX - segment0.x;
  var dy:Number = mouseY - segment0.y;
  var angle:Number = Math.atan2(dy, dx);
  segment0.rotation = angle * 180 / Math.PI;
  var w:Number = segment0.getPin().x - segment0.x;
  var h:Number = segment0.getPin().y - segment0.y;
  var tx:Number = mouseX - w;
  var ty:Number = mouseY - h;
  dx = tx - segment1.x;
  dy = ty - segment1.y;
  angle = Math.atan2(dy, dx);
  segment1.rotation = angle * 180 / Math.PI;
}
      新增代码与函数中前四行代码相似,只是使用了不同的关节与目标。
      最终,重置 segment0 的位置,它位于 segment1 的末端,因为 segment1 现在已经旋
转到了新的位置上。
private function onEnterFrame(event:Event):void {
  var dx:Number = mouseX - segment0.x;
  var dy:Number = mouseY - segment0.y;
  var angle:Number = Math.atan2(dy, dx);
  segment0.rotation = angle * 180 / Math.PI;
  var w:Number = segment0.getPin().x - segment0.x;
  var h:Number = segment0.getPin().y - segment0.y;
  var tx:Number = mouseX - w;
  var ty:Number = mouseY - h;
  dx = tx - segment1.x;
  dy = ty - segment1.y;
  angle = Math.atan2(dy, dx);
  segment1.rotation = angle * 180 / Math.PI;
  segment0.x = segment1.getPin().x;
  segment0.y = segment1.getPin().y;
}
      测试一下这个例子,我们看到两个关节就像一个整体,向着鼠标伸来。
      现在,整理一下代码,以便可以轻松地加入更多的关节。首先将所有的 rotation 内容
放入一个名为 reach 的函数中。
private function reach(segment:Segment, xpos:Number, ypos:Number):Point {
  var dx:Number = xpos - segment.x;
  var dy:Number = ypos - segment.y;
  var angle:Number = Math.atan2(dy, dx);
  segment.rotation = angle * 180 / Math.PI;
  var w:Number = segment.getPin().x - segment.x;
  var h:Number = segment.getPin().y - segment.y;
  var tx:Number = xpos - w;
  var ty:Number = ypos - h;
  return new Point(tx,ty);
}
      注意函数的返回类型是 Point,最后一行创建并返回了一个基于 tx 和 ty 的 Point。这
样就允许我们调用 reach 函数来旋转关节,它将返回目标点,然后就可以传入下一次调用
中了。不要忘记导入 Point 类。那么 onEnterFrame 就变成这样:
private function onEnterFrame(event:Event):void {
  var target:Point = reach(segment0, mouseX, mouseY);
  reach(segment1, target.x, target.y);
  segment0.x = segment1.getPin().x;
  segment0.y = segment1.getPin().y;
}
      segment0 永远向鼠标方向伸展,segment1 向 segment0 伸展。下面我们把这个设置位
置的代码放入单独的方法 position 中:
private function position(segmentA:Segment, segmentB:Segment):void {
  segmentA.x = segmentB.getPin().x;
  segmentA.y = segmentB.getPin().y;
}
      最后使用 position(segment0, segment1); 将 segment0 固定到 segment1 的末端上。
      下面是最终的代码 TwoSegmentReach.as:
package {
  import flash.display.Sprite;
  import flash.events.Event;
  import flash.geom.Point;
  public class TwoSegmentReach extends Sprite {
   private var segment0:Segment;
   private var segment1:Segment;
   public function TwoSegmentReach() {
     init();
   }
   private function init():void {
     segment0 = new Segment(100, 20);
     addChild(segment0);
     segment1 = new Segment(100, 20);
     addChild(segment1);
     segment1.x = stage.stageWidth / 2;
     segment1.y = stage.stageHeight / 2;
     addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
private function onEnterFrame(event:Event):void {
  var target:Point = reach(segment0, mouseX, mouseY);
  reach(segment1, target.x, target.y);
  position(segment0, segment1);
}
private function reach(segment:Segment, xpos:Number, ypos:Number):Point {
  var dx:Number = xpos - segment.x;
  var dy:Number = ypos - segment.y;
  var angle:Number = Math.atan2(dy, dx);
  segment.rotation = angle * 180 / Math.PI;
  var w:Number = segment.getPin().x - segment.x;
  var h:Number = segment.getPin().y - segment.y;
  var tx:Number = xpos - w;
  var ty:Number = ypos - h;
  return new Point(tx,ty);
}
private function position(segmentA:Segment, segmentB:Segment):void {
  segmentA.x = segmentB.getPin().x;
      segmentA.y = segmentB.getPin().y;
    }
  }
}
        有了这些就可以很容易地创建一个数组,持有许多关节。MultiSegmentReach.as 就是这
样做的:
package {
  import flash.display.Sprite;
  import flash.events.Event;
  import flash.geom.Point;
  public class MultiSegmentReach extends Sprite {
    private var segments:Array;
    private var numSegments:uint = 6;
    public function MultiSegmentReach() {
      init();
    }
    private function init():void {
      segments = new Array();
      for (var i:uint = 0; i < numSegments; i++) {
        var segment:Segment = new Segment(50, 10);
        addChild(segment);
        segments.push(segment);
      }
      // 将最后一个的位置设置到舞台中心
      segment.x = stage.stageWidth / 2;
      segment.y = stage.stageHeight / 2;
      addEventListener(Event.ENTER_FRAME, onEnterFrame);
    }
    private function onEnterFrame(event:Event):void {
      var target:Point = reach(segments[0], mouseX, mouseY);
      for (var i:uint = 1; i < numSegments; i++) {
        var segment:Segment = segments[i];
    target = reach(segment, target.x, target.y);
  }
  for (i = numSegments - 1; i > 0; i--) {
    var segmentA:Segment = segments[i];
    var segmentB:Segment = segments[i - 1];
    position(segmentB, segmentA);
  }
}
private function reach(segment:Segment, xpos:Number, ypos:Number):Point {
  var dx:Number = xpos - segment.x;
  var dy:Number = ypos - segment.y;
  var angle:Number = Math.atan2(dy, dx);
  segment.rotation = angle * 180 / Math.PI;
  var w:Number = segment.getPin().x - segment.x;
  var h:Number = segment.getPin().y - segment.y;
  var tx:Number = xpos - w;
  var ty:Number = ypos - h;
      return new Point(tx,ty);
    }
    private function position(segmentA:Segment,
    segmentB:Segment):void {
      segmentA.x = segmentB.getPin().x;
      segmentA.y = segmentB.getPin().y;
    }
  }
}
       运行结果如图 14-5 所示。效果比开始时好多了。但是为什么关节链整天总是追着鼠标
跑呢?它似乎有一些自己的意识。让我们看看如果给它一个玩具会发生什么!

转 <wbr>第十四章 <wbr>反向运动学: <wbr>拖拽与伸展(as3.0)
图 14-5 多关节伸展
抓住一个物体
      下一个例子,还要重新用到 Ball 类,把它加入到我们的工程或类路径中。然后为小球
创建一些新的变量,用于移动。注意这段代码建立在最后一个例子的基础上,我们只需要加
入:
private var ball:Ball;
private var gravity:Number = 0.5;
private var bounce:Number = -0.9;
      在 init 方法中,创建一个 Ball 的实例并加入显示列表。
private function init():void {
  ball = new Ball();
  ball.vx = 10;
  addChild(ball);
  segments = new Array();
  for (var i:uint = 0; i < numSegments; i++) {
    var segment:Segment = new Segment(50, 10);
    addChild(segment);
    segments.push(segment);
  }
  segment.x = stage.stageWidth / 2;
  segment.y = stage.stageHeight;
  addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
      在 onEnterFrame 中调用名为 moveBall 的函数,只是将所有小球的运动代码分离出
来,为了看起来不至于混乱:
private function onEnterFrame(event:Event):void {
  moveBall();
var target:Point = reach(segments[0], mouseX, mouseY);
for (var i:uint = 1; i < numSegments; i++) {
    var segment:Segment = segments[i];
    target = reach(segment, target.x, target.y);
  }
  for (i = numSegments - 1; i > 0; i--) {
    var segmentA:Segment = segments[i];
    var segmentB:Segment = segments[i - 1];
    position(segmentB, segmentA);
  }
}
       这就是那个函数:
private function moveBall():void {
  ball.vy += gravity;
  ball.x += ball.vx;
  ball.y += ball.vy;
  if (ball.x + ball.radius > stage.stageWidth) {
    ball.x = stage.stageWidth - ball.radius;
    ball.vx *= bounce;
  } else if (ball.x - ball.radius < 0) {
    ball.x = ball.radius;
    ball.vx *= bounce;
  }
  if (ball.y + ball.radius > stage.stageHeight) {
    ball.y = stage.stageHeight - ball.radius;
    ball.vy *= bounce;
  } else if (ball.y - ball.radius < 0) {
    ball.y = ball.radius;
    ball.vy *= bounce;
  }
}
     然后改变 onEnterFrame 函数中的第二行,让关节去抓住这个小球的实例,而不是鼠标:
var target:Point = reach(segments[0], ball.x, ball.y);
     OK,完成。运行结果如图 14-6 所示。现在小球来回地反弹,而手臂紧紧地跟随它。
很疯狂对吗?

转 <wbr>第十四章 <wbr>反向运动学: <wbr>拖拽与伸展(as3.0)
图 14-6 就像在玩儿球


     但是,我们还可以做得更好。现在,手臂很好地与小球发生接触,但是小球全然无视手
臂的作用。下面来给它们加入一些交互。
加入一些交互
       小球与手臂如何交互取决于我们想要什么样的交互。但是,不论做什么,首先需要的就
是碰撞检测。然后才能在碰撞时产生交互。同时,我们要将这段内容放到单独的一个函数中
去,从 onEnterFrame 中进行调用。
private function onEnterFrame(event:Event):void {
  moveBall();
  var target:Point = reach(segments[0], ball.x, ball.y);
  for (var i:uint = 1; i < numSegments; i++) {
    var segment:Segment = segments[i];
    target = reach(segment, target.x, target.y);
  }
  for (i = numSegments - 1; i > 0; i--) {
    var segmentA:Segment = segments[i];
    var segmentB:Segment = segments[i - 1];
    position(segmentB, segmentA);
  }
  checkHit();
}
       我将这个函数命名为 checkHit,并将它放到主函数的最后,因此在调用它时所有的位
置都已经完成了动作。
       下面开始 checkHit 函数:
public function checkHit():void {
  var segment:Segment = segments[0];
  var dx:Number = segment.getPin().x - ball.x;
  var dy:Number = segment.getPin().y - ball.y;
  var dist:Number = Math.sqrt(dx * dx + dy * dy);
  if (dist < ball.radius) {
    // 从这里开始交互
  }
}
       首先获得第一支手臂的枢轴端到小球的距离,       并使用距离碰撞检测来判断是否与小球产
生了碰撞。
       下面,回到刚才那个问题,当碰撞发生时应该做些什么。以下是我的计划:手臂会把小
球抛到空中(负 y 速度)     ,并且随机地在 x 轴上进行移动(随机 x 速度)    ,程序如下:
public function checkHit():void {
  var segment:Segment = segments[0];
  var dx:Number = segment.getPin().x - ball.x;
  var dy:Number = segment.getPin().y - ball.y;
  var dist:Number = Math.sqrt(dx * dx + dy * dy);
  if (dist < ball.radius) {
    ball.vx += Math.random() * 2 - 1;
    ball.vy -= 1;
  }
}
       效果非常好,最终的代码可在 PlayBall.as 中找到。我居然让它自己运行了一整夜,第
二天早上,手臂还在玩它的玩具!但是不要这个程序当作任何的“标准”。我们还可以让手
臂抓住小球并向目标投掷,或者像一个篮球游戏?或者让两个手臂相互传球?总之,就是进
行不同的交互。大家手头儿上肯定很多的玩具, 现在就可以让它们在这儿做些有趣的事情了。
使用标准的反向运动学方法
    我要对您说老实话。前面所描述的计算反向运动学的方法完全都是我自创的。在我第一
次做出这些效果时,我甚至还不知道这就叫做反向运动学。我只是想让某件东西去抓住其它
的物体,为了实现这个目的,就不得不让每个部件各自做一些事情,就在无意间,将它制作
成了一个系统,可以轻松地复制或加入其它物体。程序运行得非常好,效果也很不错,也没
有毁掉 CPU,我对它很满意,希望您也一样。听到这里您也许有些惊讶,但我不是第一个
思考这个问题的人。一些高智商并且受过更多数学公式训练的人,已经解决了这个问题并总
结出了可选方案,它们也许更加符合自然界物体的真实运动。所以让我们看一下“标准”的
反向运动学方法。随后,我们会有两种不同的方法可以自由选择。

余弦定理介绍
    反向运动学常用的方法,如本节标题所示,叫做余弦定理。又是三角学?是的。回忆一
下第三章,所有的例子都使用直角三角形——三角形中有一个角是直角(90 度)。这样的三
角形规则非常简单:正弦等于对边比斜边,余弦等于邻边比斜边,等等。整本书中我都广泛
地应用了这套规则。
    但是,如果有一个三角形没有 90 度的角,该怎么办呢?我们这就样被排斥了吗?肯定
不会,一个古希腊人也想到了这一点,并给出了余弦定理来帮助我们计算出各种三角形的角
度以及边长。当然,这个定理会有些复杂,但是如果您对三角有足够的了解,那么您就可以
给出解决方案了。
    大家也许会问“这玩艺儿对反向运动学有什么用?”好,请看图 14-7。

转 <wbr>第十四章 <wbr>反向运动学: <wbr>拖拽与伸展(as3.0)
图 14-7 两个关节形成的一个三角形,a, b, c 为三条边,A, B, C 为三个角
     这里有两个关节。左边的那个是固定端。它是固定住的,因此它的位置是已知的。我们
要将自由端放到图中标出的位置。这样就形成了一个任意三角形。
    已知条件有哪些呢?我们可以轻松地求出两个端点间的距离—— c 边。还知道每个关
节的长度—— a, b 边。因此就知道了所有三条边的长度。
我们需要知道这个三角形的什么呢?只需要知道两个关节的两个角度—— 角 B 和 C。这
是由余弦定理帮助我们做的。下面我给大家介绍一下:
     c2 = a2 + b2 - 2 * a * b * cos C
现在,我们要知道角 C,所以可以将它从一条边中分离出来。这里就不一一列出每一步了,
因为这是基本的代数学。最终得到:
     C = acos ((a2 + b2 - c2) / (2 * a * b))
其中,acos 是反余弦函数。角的余弦给我们一个比率,或小数。而这个比率的反余弦,则
会反过来给出角度。Flash 中函数表示为 Math.acos()。只要我们知道 a, b, c 边,就可以求
出角 C。同理,还需要知道角 B。余弦定理是这样说的:
    b2 = a2 + c2 - 2 * a * c * cos B
化简后得出:
    B = acos((a2 + c2 - b2)/ (2 * a * c))
转化成 ActionScript 就是:
    B = Math.acos((a * a + c * c - b * b) / (2 * a * c));
    C = Math.acos((a * a + b * b - c * c) / (2 * a * b));
现在我们几乎知道了所有需要设置物体位置的条件。之所以说几乎,是因为角 B 和 C 并
不是关节真正的 rotation。请看下一张图 14-8。

转 <wbr>第十四章 <wbr>反向运动学: <wbr>拖拽与伸展(as3.0)

 

图 14-8 计算 seg1 的 rotation


     角 B 求出来了,我们现在需要知道 seg1 实际要旋转多少。这个角度是从 0 或水平面
开始的,应该是角 B 与 D 之和。幸运的是,我们可以通过计算固定端与自由端的夹角得
出角 D,如图 14-9 所示。


转 <wbr>第十四章 <wbr>反向运动学: <wbr>拖拽与伸展(as3.0)

图 14-9 计算 seg0 的 rotation
     我们求得了角 C,但它只是相对于 seg1 的,需要将 seg1 的 rotation 加上 180 再加
上 C。我将这个角叫做角 E。
     OK,讲得够多了。让我们来看代码,这样就更加清晰了。
ActionScript 余弦定理
      先给出大段的反向运动学的代码,稍后进行解释。以下是代码(可在 Cosines.as 中找
到):
package {
  import flash.display.Sprite;
  import flash.events.Event;
  import flash.geom.Point;
  public class Cosines extends Sprite {
   private var segment0:Segment;
   private var segment1:Segment;
    public function Cosines() {
      init();
    }
    private function init():void {
      segment0 = new Segment(100, 20);
      addChild(segment0);
      segment1 = new Segment(100, 20);
      addChild(segment1);
      segment1.x = stage.stageWidth / 2;
      segment1.y = stage.stageHeight / 2;
      addEventListener(Event.ENTER_FRAME, onEnterFrame);
    }
    private function onEnterFrame(event:Event):void {
      var dx:Number = mouseX - segment1.x;
      var dy:Number = mouseY - segment1.y;
      var dist:Number = Math.sqrt(dx * dx + dy * dy);
      var a:Number = 100;
      var b:Number = 100;
      var c:Number = Math.min(dist, a + b);
      var B:Number = Math.acos((b * b - a * a - c * c) / (-2 * a * c));
      var C:Number = Math.acos((c * c - a * a - b * b) / (-2 * a * b));
      var D:Number = Math.atan2(dy, dx);
      var E:Number = D + B + Math.PI + C;
      segment1.rotation = (D + B) * 180 / Math.PI;
      segment0.x = segment1.getPin().x;
      segment0.y = segment1.getPin().y;
      segment0.rotation = E * 180 / Math.PI;
    }
  }
}
下面是这个过程:
1. 获得 segment1 到鼠标的距离。
2. 获得三条边的长度。 a, b 边很简单。它们都等于 100,因为这就是我们创建关节时所给
         (大家可以删掉 100 这个数,让代码变得更加动态;这里我只是为了简洁。 c 边 
的长度。
等于距离或 a + b 中最小的值。   这是因为三角形中的一条边不能大于其它两条边之和。      如果
不相信,请试着画出一个这样的图形。如果从固定端到鼠标的距离是 200,而两个关节的长
度加起来只有 120,就不能使用距离作为边长。
3. 使用余弦定理计算出角 B 和 C,再用 Math.atan2 计算出角 D。角 E,如同我们前面提
到的,它等于 D + B + 180 + C。当然,在代码中要用 Math.PI 弧度代替 180。
4. 如图 14-9 中, D + B 角转换为角度制,
                   将                           就得到了 seg1 的 rotation。接着计算出 seg1
的末端,并将 seg0 放置在此。
5. 最后,seg0 的 rotation 等于角 E,转换为角度制。
       这样我们就有了:使用余弦定理的反向运动学。大家也许注意到了连接处永远只向一
个方向弯曲。  这对于要建立一个肘部或膝关节也许是件好事,    因为它们只能向一个方向弯曲。
       当我们要可以特意计算出这样的角度时,那么这个问题就有两种解决方法:向一个方
向弯曲,或向另一个方向弯曲。我们已经计算过一个方向的弯曲,通过加 D 和 B,再加 C
来完成。如果将它们全部减去,效果相同,但是手臂的弯曲方向是相反的。
private function onEnterFrame(event:Event):void {
  var dx:Number = mouseX - segment1.x;
  var dy:Number = mouseY - segment1.y;
  var dist:Number = Math.sqrt(dx * dx + dy * dy);
  var a:Number = 100;
  var b:Number = 100;
  var c:Number = Math.min(dist, a + b);
  var B:Number = Math.acos((b * b - a * a - c * c) / (-2 * a * c));
  var C:Number = Math.acos((c * c - a * a - b * b) / (-2 * a * b));
  var D:Number = Math.atan2(dy, dx);
  var E:Number = D - B + Math.PI - C;
  segment1.rotation = (D - B) * 180 / Math.PI;
  segment0.x = segment1.getPin().x;
  segment0.y = segment1.getPin().y;
  segment0.rotation = E * 180 / Math.PI;
}
      如果要想让两个方向都能弯曲,我们就需要加入一些逻辑条件, 比如“如果在这个位置,
则向这个方向弯曲;反之,向另一个方向弯曲。”不幸的是,我只能个大家一些简短的余弦
定理的介绍。如果大家对这方面感兴趣,我相信您一定还会找更多相关的知识。快速的网络
搜索“反向运动学”给了出多达 90,000 条搜索结果。是啊,我们一定能从中挖掘出一些东
西来!

 

 

 

本章重要公式
     标准的反向运动学形式,使用余弦定理公式。
余弦定理:
a2 = b2 + c2 - 2 * b * c * cos A
b2 = a2 + c2 - 2 * a * c * cos B
c2 = a2 + b2 - 2 * a * b * cos C
ActionScript 的余弦定理:
A = Math.acos((b * b + c * c - a * a) / (2 * b * c));
B = Math.acos((a * a + c * c - b * b) / (2 * a * c));
C = Math.acos((a * a + b * b - c * c) / (2 * a * b));

 

(如果要转载请注明出处http://blog.sina.com.cn/jooi,谢谢)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值