Unity でキャラクタの足の位置を地面の形状に合わせてみた - 凹みTips

凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity でキャラクタの足の位置を地面の形状に合わせてみた

はじめに

段差や坂を登る時などに単にアニメーションさせているだけではコライダの位置に合わせてキャラクタが上下するだけで、片方の足は地面に付いているけど、もう片方の足の位置は地面から離れているといった不自然な表現になってしまいます。

これを解決するには、アニメーションした後に地面の形状に応じて足の位置と傾きを上書きしてあげれば良いわけです。Unity では MonoBehaviourOnAnimatorIK() などのタイミングで IK(Inverse Kinematics)を利用することで、手前の関節も位置も考慮して両手両足の位置を上書きをすることが出来ます。これを利用すると簡単に足を地面に沿わせることが出来ます。前半はこれについて解説します。

地面の形状に合うようにつけるのは簡単なのですが、動いている時も自然な見た目になるようにしようとすると色々と面倒になります。自分で作るのも大変なので、有料になってしまいますが Unity の有名なアセットの一つである Final IK を使ってセットアップする方法について後半解説します。

スクリーンショット

未使用時

f:id:hecomi:20150422222028p:plain

Unity 標準の Animator IK

f:id:hecomi:20150422222001p:plain

Final IK(Grounder FBBIK)

f:id:hecomi:20150422222440p:plain

デモ

Final IK のデモです。

Unity 標準の Animator IK の利用

4.x では IK はプロ版の機能でしたが 5.x からフリー版でも使えるようになりました。

Unity では MonoBehaviour.OnAnimatorIK() および Unity 5 から導入された StateMachineBehaviour.OnStateIK() のタイミングで、IK を利用して両手両足の位置をアニメーション更新後に上書きして指定することが出来ます。

以下、地面に設置させるサンプルです。

using UnityEngine;
using System.Collections;

public class FootIK : MonoBehaviour 
{
    private Animator animator_;

    public bool  isDrawDebug = false; 
    public float heelOffsetZ = 0f;
    public float toeOffsetZ  = 0f;
    public float rayLength   = 0.2f;

    // 身体の各ボーンの位置は Animator から取れるのでエイリアスを作っておくと便利
    private Transform leftFoot  { get { return animator_.GetBoneTransform(HumanBodyBones.LeftFoot);  } }
    private Transform rightFoot { get { return animator_.GetBoneTransform(HumanBodyBones.RightFoot);  } }
    private Transform leftToe   { get { return animator_.GetBoneTransform(HumanBodyBones.LeftToes); } }
    private Transform rightToe  { get { return animator_.GetBoneTransform(HumanBodyBones.RightToes); } }


    void Start()
    {
        animator_ = GetComponent<Animator>();
    }


    void OnAnimatorIK()
    {
        // IK の位置から踵・つま先のオフセットを設定
        var heelOffset   = Vector3.up * heelOffsetZ;
        var toeOffset    = Vector3.up * toeOffsetZ;
        var leftHeelPos  = leftFoot.position  + heelOffset;
        var leftToePos   = leftToe.position   + toeOffset;
        var rightHeelPos = rightFoot.position + heelOffset;
        var rightToePos  = rightToe.position  + toeOffset;

        // 足の位置を IK に従って動かす
        var leftIkMoveLength  = UpdateFootIk(AvatarIKGoal.LeftFoot,  leftHeelPos,  leftToePos);
        var rightIkMoveLength = UpdateFootIk(AvatarIKGoal.RightFoot, rightHeelPos, rightToePos);

        // 身体の位置を下げないと IK で移動できないので
        // IK で移動させた差分だけ身体を下げる
        animator_.bodyPosition += Mathf.Max(leftIkMoveLength, ightIkMoveLength) * Vector3.down;
    }


    float UpdateFootIk(AvatarIKGoal goal, Vector3 heelPos, Vector3 toePos)
    {
        // レイを踵から飛ばす(めり込んでた時も平気なようにちょっと上にオフセットさせる)
        RaycastHit ray;
        var from   = heelPos + Vector3.up * rayLength;
        var to     = Vector3.down;
        var length = 2 * rayLength;

        if (Physics.Raycast(from, to, out ray, length)) {
            // レイが当たった場所を踵の場所にする
            var nextHeelPos = ray.point - Vector3.up * heelOffsetZ;
            var diffHeelPos = (nextHeelPos - heelPos);

            // Animator.SetIKPosition() で IK 位置を動かせるので、
            // 踵の移動分だけ動かす
            // 第1引数は AvatarIKGoal という enum(LeftFoot や RightHand など)
            animator_.SetIKPosition(goal, animator_.GetIKPosition(goal) + diffHeelPos);
            // Animator.SetIKPositionWeight() では IK のブレンド具合を指定できる
            // 本当は 1 固定じゃなくて色々フィルタ掛けると良いと思う
            animator_.SetIKPositionWeight(goal, 1f);

            // 踵からつま先の方向に接地面が上になるように向く姿勢を求めて
            // IK に反映させる
            var rot = GetFootRotation(nextHeelPos, toePos, ray.normal);
            animator_.SetIKRotation(goal, rot);
            animator_.SetIKRotationWeight(goal, 1f);

            // レイを確認用に描画しておくと分かりやすい
            if (isDrawDebug) {
                Debug.DrawLine(heelPos, ray.point, Color.red);
                Debug.DrawRay(nextHeelPos, rot * Vector3.forward, Color.blue);
            }

            return diffHeelPos.magnitude;
        }

        return 0f;
    }


    Quaternion GetFootRotation(Vector3 heelPos, Vector3 toePos, Vector3 slopeNormal)
    {
        // つま先の位置からレイを下に飛ばす
        RaycastHit ray;
        if (Physics.Raycast(toePos, Vector3.down, out ray, 2 * rayLength)) {
            if (isDrawDebug) {
                Debug.DrawLine(toePos, ray.point, Color.red);
            }
            var nextToePos = ray.point + Vector3.up * toeOffsetZ;
            // つま先方向に接地面の法線を上向きとする傾きを求める
            return Quaternion.LookRotation(nextToePos - heelPos, slopeNormal);
        }
        // レイが当たらなかったらつま先の位置はそのままで接地面方向に回転だけする
        return Quaternion.LookRotation(toePos - heelPos, slopeNormal);
    }
}

これで冒頭のスクショのようになります。

ただ、このままでは動かすと地面に近づいた時に足がアニメーションで地面につく前に IK で地面についてしまうのでビッタンビッタンした動きになってしまいます。これを防ぐために足の上下速度に応じてフィルタを掛けたりしてあげたり、StateMachineBehaviour.OnStateIK() の方で特定のモーションの時だけ接地するようにしてあげたりする必要があります。また走っていて足が垂直になるようなときに、上記スクリプトでは踵-つま先方向を保ったまま地面につけようとする結果、すごい角度で地面に接地した感じになってしまいます。これを防ぐために足の角度制限をつけてあげたりする必要があると思います。また、Animator IK は膝位置を調整できないので、やけに内股になってしまったりします。

Final IK を利用する

こういった問題点は、Final IK に同梱されている Grounder FBBIK を使うと解決します。Final IK は $90 のアセットで Full Body な IK や人以外の IK も出来たりと Unity の標準の IK と比べるとかなり高機能になっています。動画も沢山上がっているので見てると買いたくなると思います。

Grounder FBBIK はこんな感じで使えます。

基本的には Full Body Biped IK(人型の全身の IK)と Grounder FBBIK をアタッチして、Grounder FBBIKIkFull Body Biped IK をドラッグ&ドロップするだけで良いのですが、セットアップが失敗することがあります。

f:id:hecomi:20150423013639p:plain

赤や黄色のジョイントがダメなところです。このまま動かすととても残念な感じになります。

f:id:hecomi:20150423014041p:plain

原因はピンと伸びていることのようで、以下の動画の後半部のように微妙に関節を動かしてあげると解決します。

f:id:hecomi:20150423014135p:plain

とても簡単です。

おわりに

Animator 周りの知識まだまだ乏しいのですが、勉強しながら、動きが綺麗でかつ色々出来る面白いキャラクタコントローラ作りたいです。