今回はキャラクターの足の位置と角度を地面に合わせる機能を作ってみたいと思います。
今回
こちらの動画を参考にさせていただきました。
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 | using UnityEngine; using System.Collections; using System; public class FootIK : MonoBehaviour { private CharacterController characterController; private Animator animator; // テスト用のIKのOn・Offスイッチ [SerializeField] private bool useIK = true; // IKで角度を有効にするかどうか [SerializeField] private bool useIKRot = true; // 右足のウエイト private float rightFootWeight = 0f; // 左足のウエイト private float leftFootWeight = 0f; // 右足の位置 private Vector3 rightFootPos; // 左足の位置 private Vector3 leftFootPos; // 右足の角度 private Quaternion rightFootRot; // 左足の角度 private Quaternion leftFootRot; // 右足と左足の距離 private float distance; // 足を付く位置のオフセット値 [SerializeField] private float offset = 0.1f; // レイを飛ばす距離 [SerializeField] private float rayRange = 1f; // レイを飛ばす位置の調整値 [SerializeField] private Vector3 rayPositionOffset = Vector3.up * 0.3f; void Start() { characterController = GetComponent<CharacterController>(); animator = GetComponent<Animator>(); } void OnAnimatorIK() { // IKを使わない場合はこれ以降なにもしない if (!useIK) { return; } // アニメーションパラメータからIKのウエイトを取得 rightFootWeight = animator.GetFloat("RightFootWeight"); leftFootWeight = animator.GetFloat("LeftFootWeight"); // 右足用のレイの視覚化 Debug.DrawRay(animator.GetIKPosition(AvatarIKGoal.RightFoot) + rayPositionOffset, -transform.up * rayRange, Color.red); // 右足用のレイを飛ばす処理 var ray = new Ray(animator.GetIKPosition(AvatarIKGoal.RightFoot) + rayPositionOffset, -transform.up); RaycastHit hit; if (Physics.Raycast(ray, out hit, rayRange, LayerMask.GetMask("Field"))) { rightFootPos = hit.point; // 右足IKの設定 animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, rightFootWeight); animator.SetIKPosition(AvatarIKGoal.RightFoot, rightFootPos + new Vector3(0f, offset, 0f)); if (useIKRot) { rightFootRot = Quaternion.FromToRotation(transform.up, hit.normal) * transform.rotation; animator.SetIKRotationWeight(AvatarIKGoal.RightFoot, rightFootWeight); animator.SetIKRotation(AvatarIKGoal.RightFoot, rightFootRot); } } // 左足用のレイを飛ばす処理 ray = new Ray(animator.GetIKPosition(AvatarIKGoal.LeftFoot) + rayPositionOffset, -transform.up); // 左足用のレイの視覚化 Debug.DrawRay(animator.GetIKPosition(AvatarIKGoal.LeftFoot) + rayPositionOffset, -transform.up * rayRange, Color.red); if (Physics.Raycast(ray, out hit, rayRange, LayerMask.GetMask("Field"))) { leftFootPos = hit.point; // 左足IKの設定 animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, leftFootWeight); animator.SetIKPosition(AvatarIKGoal.LeftFoot, leftFootPos + new Vector3(0f, offset, 0f)); if (useIKRot) { leftFootRot = Quaternion.FromToRotation(transform.up, hit.normal) * transform.rotation; animator.SetIKRotationWeight(AvatarIKGoal.LeftFoot, leftFootWeight); 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 27 28 29 30 31 32 | private CharacterController characterController; private Animator animator; // テスト用のIKのOn・Offスイッチ [SerializeField] private bool useIK = true; // IKで角度を有効にするかどうか [SerializeField] private bool useIKRot = 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; // レイを飛ばす位置の調整値 [SerializeField] private Vector3 rayPositionOffset = Vector3.up * 0.3f; |
useIKは足のIKを使うかどうかで、インスペクタでチェックのオン・オフを出来るようにします。
useIKRotは足の角度を地面に合わせるかどうかです。
rightFootWeightとleftFootWeightは両足のIKのウエイトを入れます。
rightFootPos、leftFootPosは両足の位置、rightFootRot、leftFootRotは両足の角度を入れます。
offsetは足を置く位置のオフ設置値です。
rayRangeは両足から下側にレイを飛ばす距離です。
rayPositionOffsetは足が接地する地面の位置と角度を取得する時にレイを足先から飛ばしますが、その位置の上へのオフセット値を指定します。
これは足先からレイを飛ばすとアニメーションと接地部分の関係から地面の下に足先が来てレイが地面の下から出てしまう現象の対処のために足先より上からレイを飛ばす為の調整値です。
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 | void Start() { characterController = GetComponent<CharacterController>(); animator = GetComponent<Animator>(); } void OnAnimatorIK() { // IKを使わない場合はこれ以降なにもしない if (!useIK) { return; } // アニメーションパラメータからIKのウエイトを取得 rightFootWeight = animator.GetFloat("RightFootWeight"); leftFootWeight = animator.GetFloat("LeftFootWeight"); // 右足用のレイの視覚化 Debug.DrawRay(animator.GetIKPosition(AvatarIKGoal.RightFoot) + rayPositionOffset, -transform.up * rayRange, Color.red); // 右足用のレイを飛ばす処理 var ray = new Ray(animator.GetIKPosition(AvatarIKGoal.RightFoot) + rayPositionOffset, -transform.up); RaycastHit hit; if (Physics.Raycast(ray, out hit, rayRange, LayerMask.GetMask("Field"))) { rightFootPos = hit.point; // 右足IKの設定 animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, rightFootWeight); animator.SetIKPosition(AvatarIKGoal.RightFoot, rightFootPos + new Vector3(0f, offset, 0f)); if (useIKRot) { rightFootRot = Quaternion.FromToRotation(transform.up, hit.normal) * transform.rotation; animator.SetIKRotationWeight(AvatarIKGoal.RightFoot, rightFootWeight); animator.SetIKRotation(AvatarIKGoal.RightFoot, rightFootRot); } } // 左足用のレイを飛ばす処理 ray = new Ray(animator.GetIKPosition(AvatarIKGoal.LeftFoot) + rayPositionOffset, -transform.up); // 左足用のレイの視覚化 Debug.DrawRay(animator.GetIKPosition(AvatarIKGoal.LeftFoot) + rayPositionOffset, -transform.up * rayRange, Color.red); if (Physics.Raycast(ray, out hit, rayRange, LayerMask.GetMask("Field"))) { leftFootPos = hit.point; // 左足IKの設定 animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, leftFootWeight); animator.SetIKPosition(AvatarIKGoal.LeftFoot, leftFootPos + new Vector3(0f, offset, 0f)); if (useIKRot) { leftFootRot = Quaternion.FromToRotation(transform.up, hit.normal) * transform.rotation; animator.SetIKRotationWeight(AvatarIKGoal.LeftFoot, leftFootWeight); animator.SetIKRotation(AvatarIKGoal.LeftFoot, leftFootRot); } } } |
OnAnimatorIKはAnimatorControllerのレイヤーでIK Passにチェックを入れていると呼ばれるメソッドです。
useIKがfalseの時はそれ以降の処理をしないようにreturnします。
両足からレイを下側に飛ばし、地面に接触していたらその位置と角度を保持します。
レイの当たった位置はhit.pointで得られますが、角度は上側の方向と接触面の表面の方向から計算した角度に自身の足の角度をかけて求めます。
得られた情報はそれぞれのフィールドに保持しておきます。
1 2 3 | animator.GetIKPosition (AvatarIKGoal.RightFoot) |
で右足のIKの位置を取得出来ます。
useIKRotがtrueの時だけ足の角度を地面の角度に合わせます。
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 | // コライダの中心位置を変更するかどうか [SerializeField] private bool isChangeColPos = true; // 右足と左足の距離 private float distance; // コライダの中心位置 private Vector3 defaultCenter; // コライダの位置を調整する時のスピード [SerializeField] private float smoothing = 100f; void Start () { defaultCenter = characterController.center; } void OnAnimatorIK () { // コライダの中心位置を下げる場合 if (isChangeColPos) { // 両足のY軸の高さの差を計算 distance = Mathf.Abs((float)Math.Round(rightFootPos.y, 2) - (float)Math.Round(leftFootPos.y, 2)); characterController.center = Vector3.Lerp(characterController.center, new Vector3(0f, defaultCenter.y + distance, 0f), smoothing * Time.deltaTime); } } |
isChangeColPosはコライダの位置を調整するかどうかの設定です。
distanceは右足と左足の距離の半分を入れます。
defaultCenterはコライダのCenterの位置の初期位置です。
smoothingはコライダの位置を調整する時のスピードです。
OnAnimatorIKメソッド内の最後に処理を追加します。
Math.Roundを使って両足のY軸の位置を小数点以下2桁までに丸めて差の絶対値を計算します。
CharacterControllerのcenterの位置のY軸の位置をデフォルト値からdistanceの値を足して調整します。
地面となるゲームオブジェクトにはFieldレイヤーを設定し、確認してみてください。
体の重心位置を変化させる方法
コライダのcenterを変更することで足の位置を地面に合わせるようにしましたが、AnimatorのbodyPositionの位置を変更することでも出来ます。
こちらの場合はキャラクターの重心の位置を変更するのでコライダを調整する必要がありません。
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 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | using UnityEngine; using System.Collections; using System; public class NewFootIK : MonoBehaviour { private Animator animator; // テスト用のIKのOn・Offスイッチ [SerializeField] private bool useIK = true; // IKで角度を有効にするかどうか [SerializeField] private bool useIKRot = true; // 地面とするレイヤー [SerializeField] private LayerMask fieldLayer; // 右足のウエイト private float rightFootWeight = 0f; // 左足のウエイト private float leftFootWeight = 0f; // 右足の位置 private Vector3 rightFootIKPosition; // 左足の位置 private Vector3 leftFootIKPosition; // 右足の角度 private Quaternion rightFootRot; // 左足の角度 private Quaternion leftFootRot; // 右足と左足の距離 private float distance; // 足を付く位置のオフセット値 [SerializeField] private Vector3 offset = new Vector3(0f, 0.06f, 0f); // コライダの中心位置 private Vector3 defaultCenter; // レイを飛ばす距離 [SerializeField] private float rayRange = 1f; // 体の重心を調整する時のスピード [SerializeField] private float bodyPositionSpeed = 50f; // レイを飛ばす位置の調整値 [SerializeField] private Vector3 rayPositionOffset = Vector3.up * 0.3f; // 体の重心を変更するかどうか [SerializeField] private bool isChangeBodyPosition = true; // 前回の体の重心位置 private Vector3 preBodyPosition; // 足のレイが地面についているかどうか private bool rightFootGrounded; private bool leftFootGrounded; void Start() { animator = GetComponent<Animator>(); } void OnAnimatorIK() { // IKを使わない場合はこれ以降なにもしない if (!useIK) { return; } // アニメーションパラメータからIKのウエイトを取得 rightFootWeight = animator.GetFloat("RightFootWeight"); leftFootWeight = animator.GetFloat("LeftFootWeight"); // 右足用のレイの視覚化 Debug.DrawRay(animator.GetIKPosition(AvatarIKGoal.RightFoot) + rayPositionOffset, Vector3.down * rayRange, Color.red); // 右足用のレイを飛ばす処理 var ray = new Ray(animator.GetIKPosition(AvatarIKGoal.RightFoot) + rayPositionOffset, Vector3.down); RaycastHit hit; if (Physics.Raycast(ray, out hit, rayRange, fieldLayer)) { rightFootGrounded = true; rightFootIKPosition = hit.point; // 右足IKの設定 animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, rightFootWeight); animator.SetIKPosition(AvatarIKGoal.RightFoot, rightFootIKPosition + offset); if (useIKRot) { rightFootRot = Quaternion.FromToRotation(transform.up, hit.normal) * transform.rotation; animator.SetIKRotationWeight(AvatarIKGoal.RightFoot, rightFootWeight); animator.SetIKRotation(AvatarIKGoal.RightFoot, rightFootRot); } } else { rightFootGrounded = false; } // 左足用のレイを飛ばす処理 ray = new Ray(animator.GetIKPosition(AvatarIKGoal.LeftFoot) + rayPositionOffset, Vector3.down); // 左足用のレイの視覚化 Debug.DrawRay(animator.GetIKPosition(AvatarIKGoal.LeftFoot) + rayPositionOffset, Vector3.down * rayRange, Color.red); if (Physics.Raycast(ray, out hit, rayRange, fieldLayer)) { leftFootGrounded = true; leftFootIKPosition = hit.point; // 左足IKの設定 animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, leftFootWeight); animator.SetIKPosition(AvatarIKGoal.LeftFoot, leftFootIKPosition + offset); if (useIKRot) { leftFootRot = Quaternion.FromToRotation(transform.up, hit.normal) * transform.rotation; animator.SetIKRotationWeight(AvatarIKGoal.LeftFoot, leftFootWeight); animator.SetIKRotation(AvatarIKGoal.LeftFoot, leftFootRot); } } else { leftFootGrounded = false; } // 体の重心を動かす場合 if (isChangeBodyPosition && rightFootGrounded && leftFootGrounded) { // 左右の足とキャラクターの足元の位置との距離を計算 var rightFootDistance = rightFootIKPosition.y - transform.position.y; var leftFootDistance = leftFootIKPosition.y - transform.position.y; // 左右の足の位置がより下にある方を距離として使う var distance = rightFootDistance < leftFootDistance ? rightFootDistance : leftFootDistance; // 体の重心を下にある方の足に合わせて下げる var nowBodyPosition = animator.bodyPosition + Vector3.up * distance; // 徐々に変更するようにしているが、たぶんコメントにしてある処理のように一気に変えても問題ない animator.bodyPosition = Vector3.Lerp(preBodyPosition, nowBodyPosition, bodyPositionSpeed * Time.deltaTime); preBodyPosition = animator.bodyPosition; //animator.bodyPosition = nowBodyPosition; } } } |
基本的には既に作ったスクリプトと同じですが、足の位置を指定した位置に移動する際に徐々に変化させるのではなくすぐさまその位置へと移動させています。
またコライダのcenterを変更していた部分をAnimatorのbodyPositionを変更するように変更しました。
体の重心を変化させるのは右足と左足からのレイが両方とも地面に接触している時だけ行う事にしました。
これは片足が地面にあり、片足が空中に浮いている場合に不必要に足が曲がってしまうのを避ける為です。
体の重心位置を変化させる時は右足とキャラクターの基点(足元)との距離と左足とキャラクターの基点との距離を計算し、より下側にある方に体の重心を移動させます。
こうすることによって坂を下っている時はより下にある足の方に重心が下がるので足を地面に接地させることが出来ます(足が届きます)。
地面と判定するレイヤーはキャラクターに取り付けたNewFootIKのインスペクタのFieldLayerの部分から選択してください。
今回変更した結果は以下のようになりました。
キャラクターの歩行アニメーションのCurveで設定しているRightFootWeightとLeftFootWeightによっては足がすり足になってしまいます。
ウエイトが1になるのが遅いと足が地面に付く前に地面にめり込んでしまうこともあるのでカーブの作り方は調整が必要です。
今回はPS4のニーアオートマタを久しぶりにやっていて2Bの足が綺麗に地面についているのを見て同じように滑らかに接地する足の機能を作りたい!と思って少し修正してみました。
終わりに
これでIKを使って足の位置と角度を地面に合わせる機能が完成しました。
今回の修正である程度機能としては出来たのではないかと思います。