第十三章介紹了一些基礎的運動學以及正向與反向運動學之間的區別。 前一章我們講了
正向運動學,本章就要學習與它關係緊密的反向運動學。涉及到的動作就是拖拽與伸展。
與正向運動學的例子相同,本章的例子也是從獨立的關節開始建立系統。 我們從單個關
節開始,然後到多個關節。首先,我會給大家示範最簡單的計算角度與位置的方法。只是在
代碼中使用基本的三角學進行大概的測算。最後,會給大家簡要地介紹使用餘弦定理的方法,
這樣計算出來的結果更加準確,但會消耗大量的計算——這就是所謂的權衡。
單物體的拖拽與伸展
前面說過,反向運動學系統可以分為兩種不同的類型:拖拽與伸展。
當系統的自由端向目標點伸展時,系統的另一端——固定端,也許是動不了的,因此如
果目標點位置超出了自由端運動的範圍,那麼自由端永遠也不能到達目標點。舉個例子,當
我們試圖抓住某個東西時,手指就朝著這個物體移動, 手腕的轉動會使我們的手指與目標位
置越來越近,肘部,肩膀和身體其它的部分也都儘可能地伸展。有時,所有這些位置的組合
將會使手指接觸到物體;有時也許不行。如果物體是來回運動的,我們的肢體就要做出即時
的反映不斷調整位置,為了讓手指能夠儘可能地夠到該物體。反向運動學將會告訴我們,如
何設定所有這些零件的位置,達到最佳的伸展效果。
另一種反向運動學是在物體被拖拽的時候。這個例子中, 自由端是被一些外部的力所拖
動的。無論何時,系統其餘的部分都緊隨其後,它們會將自己放置到自然的可能位置上。可
以想象成一個沒有知覺死屍(對不起,這是我唯一能想到的) 。抓住他的手然後拽著它走。
我們施加在對方手上的力,會傳到手腕,肘部,肩膀,以及身體的其餘部分,它們都沿
著拖拽的方向移動。這個例子中,反向運動學將告訴我們所有的這些零件是如何隨著拖拽組
合成正確的位置。
最好的理解方法就是用例子程式加以說明,每個例子都使用一個關節。我們需要用到
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 所示,運行結果。測試一下觀察關節是如何跟隨滑鼠的。即使關節離得很遠,
它都像是快要抓住滑鼠一樣。
圖 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;