tsmallfield's diary

たぶんJavaScriptのお話

簡易的なBoneアニメーションを作ってみよう

Boneアニメーションと聞くとなんだか難しそうだし
しかもそれをJavaScriptでやるなんて。。。

でも簡易的なものならなんとかなるかも!

というわけで今回は最近制作したBoneアニメーションのサンプルをご紹介します。

まずはBoneを描いてみよう

まずは<canvas>の
translate(), rotate(), moveTo(), lineTo(), stroke()
を駆使して簡単な人型のBoneを描画してみます。

ここまではとっても簡単。
さて、このBoneを歩かせるには、どうすればよいでしょう?

関節を動かすには?

とりあえず、rotate() に指定している角度を動的に変化させれば良さそうです。

でも関節の角度を一つ一つ指定し、
しかもちゃんと歩いている風に調整するのはすごい作業になりそうなので
根元の関節の角度を一つ指定したら、それが先端まで伝搬していく作りにしましょう。

そのためにはBoneが親子関係を持っている必要があります。
例えば「体」の子が「腕」でその子が「手」、という感じです。

さらに親のBoneから子のBoneに角度が伝搬する際、
それぞれに係数を掛けるとよさそうです。
例えば、肩の関節を10度動かしたら、肘の関節も10度動かすのではなく、
10 に 係数 -0.5 を掛けた値、すなわち 10 * -0.5 = -5 度動かす。。。
という具合です。

関節の角度はどうやって計算する?

歩くという動作は周期的(ある一定の動きを繰り返す)なもの。
周期的な関数と聞いてまず思い浮かぶのは 正弦波:Math.sin(), Math.cos() です。
今回はこれをうまく利用しましょう。

【横軸】を歩いた距離、
【縦軸】を関節の角度
とすればいい感じになりそうですね。

体の上下の動きもわすれずに!

人が歩いている時、体は上下に動いているので
それも関節の角度と同じく sin, cos で計算しましょう。
上下の動きは、腕や足のそれと周期が違うので注意しましょう。

サンプル

各関節の角度、係数、角度変化の周期を地道に調整し、歩いている風にしてみました。

Boneの定義をしている部分は以下になります。

this.skeleton = new Skeleton([
    new Bone('body', 150, -80, .2, [
        new Bone('head', 50, 20, 1),
        new Bone('leftArm', 90, 190, -4, [
            new Bone('leftArm2', 80, -50, 1)
        ]),
        new Bone('rightArm', 90, 190, 4, [
            new Bone('rightArm2', 80, -50, 1)
        ])
    ]),
    new Bone('leftLeg', 100, 70, -1, [
        new Bone('leftLeg2', 100, 40, 1, [
            new Bone('leftFoot', 30, -110, .1)
        ])
    ]),
    new Bone('rightLeg', 100, 70, 1, [
        new Bone('rightLeg2', 100, 40, 1, [
            new Bone('rightFoot', 30, -110, .1)
        ])
    ])
]);
new Bone(ラベル, Boneの長さ, デフォルトの関節角, 係数[, 子のBoneの配列]);

Boneの長さとデフォルトの関節角で姿勢を変えることができます。

また係数を変更することで回転方向と俊敏さを調整できます。

以下は関節角を計算している部分のコードです。
移動距離から sin, cos を用いて関節角(の差分)を計算しています。

※目標角ではなく差分を与えているのは、歩く以外の動作を同時に行えるようにするためです。
例えば、吠えるとか口から熱線を出すとか。

/**
 *  @param {number} distance
 */
function update(distance) {
    var OFFSET_Y_MAX   = -10,
        ROTATION_PHASE = 200,
        ROTATION_GAIN  = .04,
        OFFSET_Y_PHASE = 100,
        OFFSET_Y_GAIN  =   3,
        deltaRad;
    
    deltaRad = Math.cos(Math.PI * 2 * (distance % ROTATION_PHASE) / ROTATION_PHASE) * ROTATION_GAIN;
    
    this.skeleton.parts.body.rotate(deltaRad);
    this.skeleton.parts.leftLeg.rotate(deltaRad);
    this.skeleton.parts.rightLeg.rotate(deltaRad);
    
    this.destOffsetY += Math.sin(Math.PI * 2 * (distance % OFFSET_Y_PHASE) / OFFSET_Y_PHASE) * OFFSET_Y_GAIN;
}

Bone の長さや係数、周期を調整するといろんな動きができるので、ぜひお試しあれ!