johnsoncodehk 发表于 2021-7-29 21:46

站稳了没?——Unity IK的简单应用

这次为大家带来的是一个超~~~~~~~~实用功能。
当角色站在斜坡或者楼梯上的时候,为了让游戏表现得更加真实,会有让脚贴合在地面上的需求,这个需求可以通过反向动力学IK(Inverse kinematics)来实现。
一 简介

1.反向动力学IK(Inverse kinematics)可以依据某些子关节的最终位置、角度来反推节点链上其他节点的合理位置,Unity中设置了Avatar的人形角色都支持IK功能。
2.使用IK功能需要在Animator视窗对应的层里勾选IK Pass,勾选后每帧会调用脚本中的OnAnimationIK()。有五个节点进行设置:左手、右手、左脚、右脚、头部。
3.本文涉及API
(1)设置Positon及其Weight(权重)
Animator.SetIKPosition(AvatarIKGoal goal, Vector3 goalPosition);
Animator.SetIKPositionWeight(AvatarIKGoal goal, float value);
(2)设置Rotation及其Weight(权重)
Animator.SetIKRotation(AvatarIKGoal goal, Quaternion goalRotation);
Animator.SetIKRotationWeight(AvatarIKGoal goal, float value);
AvatarIKGoal:需要匹配的Avatar节点。
二 目标与实现思路

1.目标
(1)Idle状态站立时脚能贴合斜面。
(2)跑步时脚往下踩时尽可能贴合斜面。


2.思路
(1)通过往脚底射线检测是否在上斜坡/楼梯。
(2)根据射线命中的面的法线设置旋转Rotation。
(3)求命中平面上离Foot节点最近的点,根据该点设置位置Position。
(4)进一步优化:通过动画曲线调节权重。


三 具体实现

在设置IK前,把人物的Collider/CharactorController配置好(在普通平地上能双脚着地)。
1.设置脚底射线
(1)打开人形骨骼,以Foot节点(一般在脚踝处)为参考点,计算胶囊体两端半球的中心位置。
(2)从上往脚底打一个胶囊体射线,探测脚将会接触的面,把需要进行IK的面的Layer设置为IKLayer。
射线示意图
(3)代码如下():
public bool UpdateFootTarget(Transform oriFootTrans, Transform footTargetTrans, out bool isOriFootOnStair)
{
    //==========================胶囊体射线==========================
    Vector3 oriFoot_point1 = oriFootTrans.position + oriFootTrans.up * 0.3f;//以Foot节点为参考往上0.3f
    Vector3 oriFoot_point2 = oriFootTrans.position + oriFootTrans.up * 0.3f + oriFootTrans.forward*0.15f;

    //画蓝色射线,模拟胶囊体射线范围
    Debug.DrawLine(oriFoot_point1, oriFoot_point1 - oriFootTrans.up * 0.5f, Color.blue);
    Debug.DrawLine(oriFoot_point2, oriFoot_point2 - oriFootTrans.up * 0.5f, Color.blue);
    RaycastHit oriFoot_HitInfo;
    bool hasHitPoint = Physics.CapsuleCast(oriFoot_point1, oriFoot_point2, 0.01f, -oriFootTrans.up, out oriFoot_HitInfo, 0.5f, LayerMask.GetMask("IKLayer"));
    //...
}


2.设置IK参考点(footTarget)
(1)设置参考点的Rotation
①获取射线命中点的法线。
②根据法线设置Rotation:把Foot节点的UP方向转到与命中点的法线方向一致。
public bool UpdateFootTarget(Transform oriFootTrans, Transform footTargetTrans, out bool isOriFootOnStair)
{      
    //==========================设置参考点==========================
    if (hasHitPoint)    //如果有打中IKLayer
    {
      //rotation
      Quaternion temQ = Quaternion.FromToRotation(oriFootTrans.up, oriFoot_HitInfo.normal);
      footTargetTrans.rotation = temQ * oriFootTrans.rotation;
      //...
    }
    else
    {
            isOriFootOnStair = false;
    }
    return hasHitPoint;
}


(2)设置参考点的Position
①根据击中点及其法线,生成面。
②通过Plane.ClosetPointOnPlane(Vector Point)求出平面上离Foot节点最近的点tempPointOnPlane,以此为参考设置footTarget的Position,其中footOffset将在面板中调节。
代码如下:
public bool UpdateFootTarget(Transform oriFootTrans, Transform footTargetTrans, out bool isOriFootOnStair)
{      
    //==========================设置参考点==========================
    if (hasHitPoint)    //如果有打中东西
    {
      //rotation
      ...
      //position
      var tempPlane = new Plane(oriFoot_HitInfo.normal, oriFoot_HitInfo.point);   //用射线打中的点和该点的法线求脚所踩平面
      var tempPointOnPlane = tempPlane.ClosestPointOnPlane(oriFootTrans.position);//求tempPlane面上离Foot节点最近的点
      footTargetTrans.position = tempPointOnPlane + Vector3.up * footOffset;
      Debug.DrawLine(oriFootTrans.position, tempPointOnPlane, Color.green);

      Debug.DrawRay(oriFoot_HitInfo.point, oriFoot_HitInfo.normal, Color.red, 0.01f);
      isOriFootOnStair = oriFoot_HitInfo.transform.CompareTag("Stair");
    }
    //...
}


③调节footOffset(偏移)
由于Foot节点在脚踝处,因此需要通过偏移来使脚底贴到地面,脚本中public float footOffset,在Inspctor窗口根据具体情况进行调节。


3.调节Collider/CharacterController
在上楼梯的时候CharacterController可能会把角色顶起来,造成腿伸直了也不能接触地面的情况,因此需要根据实际调节Collider/CharacterController来避免,本文直接粗暴地调节center的Y值。
调节完成后记录数据,在脚本中赋值,代码如下:
public void ChangeCenter()
{
    if (characterController)
    {
      Vector3 tempPos = characterController.center;
      if (hasFootOnStair) //如果有脚在楼梯上
      {
            //根据实际楼梯调节
            characterController.center = new Vector3(tempPos.x, centerOnStair, tempPos.z);
      }
      else
      {
            characterController.center = new Vector3(tempPos.x, centerOnGround, tempPos.z);
      }
    }
}


4.通过动画曲线调节权重
如果权重一直为1,当射线一打到地面脚就会贴上去,当播放站立这类没有抬脚的动画时,没有什么大问题。
可是在跑步状态下也许会变成贴地滑行或者脚长时间被吸到地上,因此需要调节权重,牺牲一些准确度来使动画看起来尽可能的自然。
(1)添加动画状态机中 Float 类型变量:leftFootWeight和rightFootWeight,分别代表左右脚的权重。
(2)添加并调节动画曲线(参考图示操作)
①添加曲线:双击移动的动画状态>Inspector窗口>展开Curves>”+”。
②调节曲线(体力活):脚踩在地面时权重为1,其余参数根据不同动画自行调节,使动画自然。
③站立权重曲线全1即可。
(3)在脚本中,通过Animator.GetFloat(变量名)获取参数,根据变量设置权重,代码如下:
public float GetIKWeight(bool hasOriFootHitPoint, string oriFootWeight)
{
    float tempWeight;
    if (hasOriFootHitPoint)
    {
      tempWeight = animator.GetFloat(oriFootWeight);
    }
    else
    {
      tempWeight = 0;
    }
    return tempWeight;
}


5.设置IK
把调好的参考点Transform和获取的权重作为参数设置IK。
public void SetIK(AvatarIKGoal avatarIKGoal, Transform goalTargetTrans, float weight)
{
      animator.SetIKPosition(avatarIKGoal, goalTargetTrans.position);
      animator.SetIKPositionWeight(avatarIKGoal, weight);
      animator.SetIKRotation(avatarIKGoal, goalTargetTrans.rotation);
      animator.SetIKRotationWeight(avatarIKGoal, weight);      
}


6.在OnAnimationIK()中执行以上函数,大概逻辑:
(1)更新脚对应的参考目标Transform。
(2)根据是否在楼梯(Stair)上获取权重。
(3)根据上边得出的Transfrom和权重设置IK。
(4)调节Collider/Character的中心高度。
代码如下(Transform变量都从外边拖入):
public class FootIK : MonoBehaviour
{
    Animator animator;
    CharacterController characterController;
    public bool isIK;

   
    public Transform leftFootTrans;
    public Transform rightFootTrans;

   
    public Transform leftFootTarget;
    public Transform rightFootTarget;

   
    public bool hasLeftFootHitPoint;
    public bool hasRightFootHitPoint;

   
    public bool isLeftFootOnStair;
    public bool isRightFootOnStair;
    public bool hasFootOnStair;

   
    public float footOffset = 0.12f;    //Foot节点到地面偏移距离
    public float centerOnStair = 1.1f;//Character中心高度偏移
    public float centerOnGround = 1.01f;

   
    public float leftFootWeight;
    public float rightFootWeight;
   
    void Start()
    {
      animator = GetComponent<Animator>();
      characterController = GetComponent<CharacterController>();
    }

    private void OnAnimatorIK(int layerIndex)
    {
      if (!isIK && animator)
      {
            return;
      }
      //==========================UpdateFootTarget==========================
      //LeftFoot
      hasLeftFootHitPoint = UpdateFootTarget(leftFootTrans, leftFootTarget, out isLeftFootOnStair);
      //RightFoot
      hasRightFootHitPoint = UpdateFootTarget(rightFootTrans, rightFootTarget, out isRightFootOnStair);

      //==========================SetIK==========================
      hasFootOnStair = isLeftFootOnStair || isRightFootOnStair;
      //Left
      leftFootWeight = GetIKWeight(hasLeftFootHitPoint, "leftFootWeight");//获取权重
      SetIK(AvatarIKGoal.LeftFoot, leftFootTarget, leftFootWeight);

      //Right
      rightFootWeight = GetIKWeight(hasRightFootHitPoint, "rightFootWeight");
      SetIK(AvatarIKGoal.RightFoot, rightFootTarget, rightFootWeight);

      //================改变characterController的Center,防止浮空==================
      ChangeCenter();
    }
    //...
}


结语:本文通过Unity的IK功能,使人物在站立和跑动的时候脚能够贴合地面,并利用动画曲线进行优化,使移动的时候在贴合地面的同时看起来更加自然。


在游戏中有各种可以使用IK来提升游戏表现的地方(例如攀爬之类),当需求实现的难度比较高时,可以使用插件来解决,以后有机会介绍一下插件的用法。


链接:https://pan.baidu.com/s/1OpaPxZjva-XvInQ7WclLug
提取码:1zcw
————分割线————
咱们的游戏开发交流群也欢迎强势插入:869551769
有考虑参与线下游戏开发学习的,欢~~~~~~迎访问:http://www.levelpp.com/

JamesB 发表于 2021-7-29 21:50

国内unity博客讲foot ik的是真的少。不过你这个办法弄的话,相当于每个locomotion动画都要调两个动画曲线,成本太大了吧。不能通过预测落点位置调整动画吗?

ainatipen 发表于 2021-7-29 21:58

我以前也玩过,直接获取地形高度,然后用IK矫正脚的位置到贴地面
[哈哈]

RecursiveFrog 发表于 2021-7-29 21:58

我觉得用两个动画曲线解决大量函数都没有办法解决的贴合动画效果,很划算。当然,也可以把动画的曲线读取出来然后换个简单的形式实现。

jquave 发表于 2021-7-29 22:04

我的意思是这个东西理论上可以自动化生成。对animation clip预处理一下,找到基点就行了,可以自动判断当前动画阶段 原动画位置和footik矫正位置 的混合系数

ainatipen 发表于 2021-7-29 22:09

在平地上和animtor自带的footik比怎么样
页: [1]
查看完整版本: 站稳了没?——Unity IK的简单应用