今回はキャラクターの足の位置と角度を地面に合わせる機能を作ってみたいと思います。
今回
こちらの動画を参考にさせていただきました。
IKに関しては
の記事を参考にしてください。
今回の機能を作成すると、
↑のような感じで足を地面の位置と角度に合わせる事が出来ます。
片足が空中にある時は別途なんらかの処理を加える必要がありそうです。
まずは足の位置や角度を地面に合わせるやり方を考えます。
足の位置をアニメーションとは別に地面に合わせる必要があるのでIKの機能を使います。
地面に位置と角度を合わせるので、足が地面と接地している個所を調べる必要があります。
その為にPhysics.Raycastを使ってキャラクターの足から下にレイを飛ばし常に接地面を調べるようにします。
足からレイを飛ばし地面の情報を取得するスクリプト
手のIKを使った時と同じようにキャラクターにレイを飛ばす位置を設定し、そこからレイを飛ばし地面と当たった位置と角度に足のIKを設定します。
ここまでは手のIKの時と同じ事をしています。
足のIKの場合、立っているアニメーションの時はレイの当たった位置にIKのウエイトを1にして位置を設定すればいいんですが、歩いている時や走っている時に常にウエイトを1にしているとヒザ神(ロンドンハーツから引用)のような動きになってしまうのでアニメーションの動きにしたがってウエイトを変更していく必要があります。
この処理は後でやります。
既存のキャラクターに足のIKを施すスクリプトFootIKを新しく作り取りつけます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
using UnityEngine; using System.Collections; using System; public class FootIK : MonoBehaviour { private CharacterController characterController; private Animator animator; // テスト用のIKのOn・Offスイッチ [SerializeField] private bool isUseIK = true; // 右足のウエイト private float rightFootWeight = 0f; // 左足のウエイト private float leftFootWeight= 0f; // 右足の位置 private Vector3 rightFootPos; // 左足の位置 private Vector3 leftFootPos; // 右足の角度 private Quaternion rightFootRot; // 左足の角度 private Quaternion leftFootRot; // 足を付く位置のオフセット値 [SerializeField] private float offset = 0.1f; // レイを飛ばす距離 [SerializeField] private float rayRange = 1f; private bool isRightFootIK = false; private bool isLeftFootIK = false; // コライダの位置を調整する時のスピード [SerializeField] private float smoothing = 2f; void Start () { characterController = GetComponent <CharacterController> (); animator = GetComponent<Animator>(); } void OnAnimatorIK() { // 右足用のレイの視覚化 Debug.DrawRay(animator.GetIKPosition (AvatarIKGoal.RightFoot), -transform.up * rayRange, Color.red); // 右足用のレイを飛ばす処理 var ray = new Ray(animator.GetIKPosition (AvatarIKGoal.RightFoot), -transform.up); RaycastHit hit; if (Physics.Raycast (ray, out hit, rayRange, LayerMask.GetMask ("Field"))) { rightFootPos = hit.point; rightFootRot = Quaternion.FromToRotation (transform.up, hit.normal) * transform.rotation; isRightFootIK = true; } else { isRightFootIK = false; } // 左足用のレイを飛ばす処理 ray = new Ray(animator.GetIKPosition (AvatarIKGoal.LeftFoot), -transform.up); // 左足用のレイの視覚化 Debug.DrawRay(animator.GetIKPosition (AvatarIKGoal.LeftFoot), -transform.up * rayRange, Color.red); if(Physics.Raycast(ray, out hit, rayRange, LayerMask.GetMask ("Field"))) { leftFootPos = hit.point; leftFootRot = Quaternion.FromToRotation (transform.up, hit.normal) * transform.rotation; isLeftFootIK = true; } else { isLeftFootIK = false; } // IKを使う時だけ if (isUseIK) { // アニメーションの状態からウエイトを取得 rightFootWeight = animator.GetFloat ("RightFootWeight"); leftFootWeight = animator.GetFloat ("LeftFootWeight"); if (isRightFootIK) { // 右足ウエイトの設定 animator.SetIKPositionWeight (AvatarIKGoal.RightFoot, rightFootWeight); animator.SetIKRotationWeight (AvatarIKGoal.RightFoot, rightFootWeight); // 右足の位置と角度を設定 animator.SetIKPosition (AvatarIKGoal.RightFoot, rightFootPos + new Vector3 (0f, offset, 0f)); animator.SetIKRotation (AvatarIKGoal.RightFoot, rightFootRot); } if (isLeftFootIK) { // 左足ウエイトの設定 animator.SetIKPositionWeight (AvatarIKGoal.LeftFoot, leftFootWeight); animator.SetIKRotationWeight (AvatarIKGoal.LeftFoot, leftFootWeight); // 左足の位置と角度を設定 animator.SetIKPosition (AvatarIKGoal.LeftFoot, leftFootPos + new Vector3 (0f, offset, 0f)); animator.SetIKRotation (AvatarIKGoal.LeftFoot, leftFootRot); } } } } |
スクリプトを少しづつ見ていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// テスト用のIKのOn・Offスイッチ [SerializeField] private bool isUseIK = true; // 右足のウエイト private float rightFootWeight = 0f; // 左足のウエイト private float leftFootWeight= 0f; // 右足の位置 private Vector3 rightFootPos; // 左足の位置 private Vector3 leftFootPos; // 右足の角度 private Quaternion rightFootRot; // 左足の角度 private Quaternion leftFootRot; // 足を付く位置のオフセット値 [SerializeField] private float offset = 0.1f; // レイを飛ばす距離 [SerializeField] private float rayRange = 1f; private bool isRightFootIK = false; private bool isLeftFootIK = false; |
isUseIKは足のIKを使うかどうかで、インスペクタでチェックのオン・オフを出来るようにします。
rightFootWeightとleftFootWeightは両足のIKのウエイトを入れます。
rightFootPos、leftFootPosは両足の位置、rightFootRot、leftFootRotは両足の角度を入れます。
offsetは足を置く位置のオフ設置値です。
rayRangeは両足から下側にレイを飛ばす距離です。
isRightFootIKとisLeftFootIKはレイが地面に到達している時に足のIKを有効にします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
void Start () { characterController = GetComponent <CharacterController> (); animator = GetComponent<Animator>(); } void OnAnimatorIK() { // 右足用のレイの視覚化 Debug.DrawRay(animator.GetIKPosition (AvatarIKGoal.RightFoot), -transform.up * rayRange, Color.red); // 右足用のレイを飛ばす処理 var ray = new Ray(animator.GetIKPosition (AvatarIKGoal.RightFoot), -transform.up); RaycastHit hit; if (Physics.Raycast (ray, out hit, rayRange, LayerMask.GetMask ("Field"))) { rightFootPos = hit.point; rightFootRot = Quaternion.FromToRotation (transform.up, hit.normal) * transform.rotation; isRightFootIK = true; } else { isRightFootIK = false; } // 左足用のレイを飛ばす処理 ray = new Ray(animator.GetIKPosition (AvatarIKGoal.LeftFoot), -transform.up); // 左足用のレイの視覚化 Debug.DrawRay(animator.GetIKPosition (AvatarIKGoal.LeftFoot), -transform.up * rayRange, Color.red); if(Physics.Raycast(ray, out hit, rayRange, LayerMask.GetMask ("Field"))) { leftFootPos = hit.point; leftFootRot = Quaternion.FromToRotation (transform.up, hit.normal) * transform.rotation; isLeftFootIK = true; } else { isLeftFootIK = false; } // IKを使う時だけ if (isUseIK) { // アニメーションの状態からウエイトを取得 rightFootWeight = animator.GetFloat ("RightFootWeight"); leftFootWeight = animator.GetFloat ("LeftFootWeight"); if (isRightFootIK) { // 右足ウエイトの設定 animator.SetIKPositionWeight (AvatarIKGoal.RightFoot, rightFootWeight); animator.SetIKRotationWeight (AvatarIKGoal.RightFoot, rightFootWeight); // 右足の位置と角度を設定 animator.SetIKPosition (AvatarIKGoal.RightFoot, rightFootPos + new Vector3 (0f, offset, 0f)); animator.SetIKRotation (AvatarIKGoal.RightFoot, rightFootRot); } if (isLeftFootIK) { // 左足ウエイトの設定 animator.SetIKPositionWeight (AvatarIKGoal.LeftFoot, leftFootWeight); animator.SetIKRotationWeight (AvatarIKGoal.LeftFoot, leftFootWeight); // 左足の位置と角度を設定 animator.SetIKPosition (AvatarIKGoal.LeftFoot, leftFootPos + new Vector3 (0f, offset, 0f)); animator.SetIKRotation (AvatarIKGoal.LeftFoot, leftFootRot); } } } |
OnAnimatorIKはAnimatorControllerのレイヤーでIK Passにチェックを入れていると呼ばれるメソッドです。
両足からレイを下側に飛ばし、地面に接触していたらその位置と角度を保持します。
レイの当たった位置はhit.pointで得られますが、角度は上側の方向と接触面の表面の方向から計算した角度に自身の足の角度をかけて求めます。
得られた情報はそれぞれのフィールドに保持しておきます。
1 2 3 |
animator.GetIKPosition (AvatarIKGoal.RightFoot) |
で右足のIKの位置を取得出来ます。
isUseIKがtrueならばアニメーションパラメータのRightFootWeightとLeftFootWeightから値を取得し、IKのウエイトに設定します。
RightFootWeightとLeftFootWeightはアニメーションカーブの値をそのまま設定します。
アニメーションカーブはこの後作成します。
アニメーションの再生位置によってIKのウエイトを変更する
アニメーションの再生位置でIKのウエイトを変更出来るようにします。
まずは立っている状態のアニメーションIdleの場合は常にウエイトを1にして、レイの当たった位置に足を合わせるようにします。
アニメーションのCurveを使うとアニメーションの状態によってアニメーションパラメータの値を変更出来ます。
またアニメーションパラメータの名前と同じ名前をCurveの名前に指定すれば、アニメーションパラメータの値にCurveの値がそのまま設定されます。
アニメーションカーブに関しては
を参考にしてください。
まずはアニメーションパラメータを作成します。
上のようにRightFootWeightとLeftFootWeightと名前を付けます。
次にIdleの状態のアニメーションを選択します。
上がIdleに設定しているアニメーションです。これを選択し、インスペクタ上のCurveを作成します。
Curvesの+の部分をクリックしRightFootWeightとLeftFootWeightを作成します。
ここでつける名前はアニメーションパラメータと同じ名前にします。
値の部分を1と設定し、常に1になるようにします。
これでIdleのアニメーションのCurveの設定は終了です。
次に走るアニメーションのCurveを設定します。
上のようにアニメーションの動きが見えるようにウインドウを動かしておきます。
Idleのアニメーションの時と同じようにCurveにRightFootWeightとLeftFootWeightというパラメータを作成します。
次に足の着地時はウエイトを1に、離れた時はウエイトを0になるようにCurveのパラメータを変更します。
まずは右足の着地時にRightFootWeightのウエイトを1にします。
アニメーションで右足全体が着地した地点を探します。
その部分を見つけたら、RightFootWeightのKeyを足すアイコンをクリックし、パラメータを1に設定します。
今回の場合はアニメーションの最初に右足着地しているので、Keyの追加をする必要はないかもしれません。
次に右足が離れる部分までアニメーションを移動します。
右足が離れる部分でRightFootWeightのKeyを追加し、パラメータを0に設定します。
右足が離れているアニメーションの間はウエイトを0に設定し、着地したら1に設定します。
足が空中にある間から着地までのちょっとの間はウエイトが0から1になめらかに遷移するようにします。
右足の設定が終わったら、左足の設定も同じようにしてください。
設定が終わったらUnityの実行ボタンを押してください。
足のIKがうまくいくか見てみましょう。
確認の為にIdle状態の時にRunのアニメーションを設定します。(キー操作なくアニメーションの動きを確認する為)
キャラクターをコピーしFootIKをOnにしたキャラクターとOffにしたキャラクターのアニメーションを比較し、本来のアニメーションと差異がないようにCurveのパラメータを設定していく必要があります。
設定が終わったらIdleのアニメーションをIdleに戻し、IKの部分を見ていきましょう。
出来たと思ったのに足が地面に届かない
んー、まったく出来ておりません・・・・(+_+)
右足は何となくレイの当たった位置に固定されているようですが、左足がレイの当たった位置に到達しません。
キャラクターを動かすスクリプトをOffにして、
キャラクターを選択し、TransformのYの位置を下げてみます。
上のように左足もうまく接地しました。
今度は地面をキャラクターを押し上げる感じで下から上に移動させます。
上のように足の位置が接地し、角度も地面に合っています。
つまり最初の画像ではRayが地面に当たっていてIKの設定もうまくいっているはずなのに、なぜうまく接地しないのかというと体の重心が右足部分を基点にしているから左足は伸ばしているけど地面に到達しない為です。
下から地面を上げた場合は到達点をキャラクターの足によせてるので問題が出ません。
足が地面に到達しないのはコライダの影響かも
足が空中に浮いてしまう場合もあるので、両足の距離を計算しコライダの位置を変更することで対応してみます。
処理を追加していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// コライダの中心位置を変更するかどうか [SerializeField] private bool isChangeColPos = true; // 右足と左足の距離 private float distance; // コライダの中心位置 private Vector3 defaultCenter; // コライダの位置を調整する時のスピード [SerializeField] private float smoothing = 2f; void Start () { defaultCenter = characterController.center; } void OnAnimatorIK () { // コライダの中心位置を下げる場合 if (isChangeColPos) { // 止まっている時はコライダの位置を調整 if (animator.GetCurrentAnimatorStateInfo (0).IsName ("Idle")) { // 右足と左足の距離の半分を計算 distance = Mathf.Abs ((float)Math.Round (rightFootPos.y, 2) - (float)Math.Round (leftFootPos.y, 2)) * 0.5f; // コライダのCenterの位置を調整 characterController.center = Vector3.Lerp (characterController.center, new Vector3 (0f, defaultCenter.y + distance, 0f), smoothing * Time.deltaTime); // 歩いている時はデフォルト値 } else { characterController.center = defaultCenter; } } } |
isChangeColPosはコライダの位置を調整するかどうかの設定です。
distanceは右足と左足の距離の半分を入れます。
defaultCenterはコライダのCenterの位置の初期位置です。
smoothingはコライダの位置を調整する時のスピードです。
OnAnimatorIKメソッド内の最後に処理を追加し、アニメーションがIdle状態の時だけコライダの位置を調整する事にします。
Math.Roundを使って両足のY軸の位置を小数点以下2桁までに丸めて差の絶対値を計算します。
CharacterControllerのcenterの位置のY軸の位置をデフォルト値からdistanceの値を足して調整します。
Idleアニメーション以外の時はデフォルトの位置に戻します。
これで完成です。
地面となるゲームオブジェクトにはFieldレイヤーを設定し、確認してみてください。
終わりに
これでIKを使って足の位置と角度を地面に合わせる機能が完成しました。
以前は空中に浮いて見えてしまう時に強制で位置を調整してましたがやめました。
今回の修正である程度機能としては出来たのではないかと思います。
コメント
これは質問と言うより要望なのですが…。
「Rigdoll」というものをご存知でしょうか?
モデルがダランダランになるやつですよね。
それを、rigidbodyのaddforceでも使って上に飛ばしたとしましょう。
そうすると、力抜けた人形みたいに落ちてきますよね。
ここからが本題なのですが、
「どんな態勢からでも地面に手を付き足を立て、起き上がる」という機能は作れそうですか?
聞いといてなんですが、とても難しそうですね…。
やっぱりそれ専用のアニメーションを用意するしか無いんでしょうか?
RagDollの存在は知っているんですが使った事はありません。(^_^;)
試しにEthanに設定し使ってみました。
RigidbodyとJointの設定を勝手に施してくれるんですね、キャラクターの通常のアニメーションはAnimatorで制御し、攻撃を受けた時にAnimatorを無効化しRigidbodyの物理挙動にまかせるんでしょうか。
RagDollを設定したキャラをまともに動かすやり方さえわからないのであれなんですが・・・・、
攻撃を受けた時にRigidbodyの物理挙動にまかせて地面に衝突した後、キャラクターからスクリプトでレイを前方、横、後方に飛ばしてキャラクターがどの方向を向いているかを調べ、その後Animatorを有効化し、該当するアニメーションを再生するという感じになるんでしょうか。
このやり方だとうつ伏せ、仰向け、横倒しのアニメーションを作成し、体が地面に付いた時の体制から近いアニメーションを再生するという感じになるのでアニメーションを作る必要がありそうです。
その時にターゲットマッチングで地面と手等は合わせる事は出来そうです。
アニメーションを作らずIKやキャラクターの移動だけで立ち上がらせるのはなかなか難しそうですね・・・・。
手をつき足を立てというのでなければ、攻撃を受けた時のボーンの位置と角度を保持しておき、ボーンの位置と角度をLerp等を使って元に戻すというのは出来ますが(スーッと機械的に元の位置に戻すだけ)・・・、
お力になれず申し訳ありません。(._.)
いえ、とても参考になりました!
ありがとうございます!
最後のコード、rightFootWeightを二回定義しているのでエラー置きますよ
ご指摘ありがとうございます。(._.)
修正しておきました。
この記事の機能自体の見直しをしなくてはいけないんですが、今のところあまり動作がうまくいってないので、動作がうまくいったら全体的な見直しもしなければいけませんね・・・・(-_-)