文章目录
- 简介
- 实现
- Avatar FBX Import Settings
- Animator Settings
- On Animator IK
- Calculate IK Position & Rotation
- Body Position
- Apply IK Position & Rotation
简介
通过Unity内部的Mecanim动画系统实现的FootIK功能,效果如图所示,左右分别为开启和关闭FootIK的效果:
初版1.0.0代码已上传至SKFramework
框架PackageManager
中:
相关变量说明:
Enable Foot Ik
:是否启用FootIKFoot Ik Pass Layer Index
:Animator启用IKPass对应的层级Layer Mask
:射线检测时所有的层级Body Y Offset
:身体Y坐标的偏移量Body Position Lerp Speed
:身体坐标插值的速度Foot Position Lerp Speed
:脚部坐标插值的速度Raycast Distance
:射线检测的最大距离Raycast Origin Height
:射线检测的高度
实现
Avatar FBX Import Settings
- Animation Type: 需要Humanoid人形动画
- Avatar Configuration:确保配置正确
Animator Settings
- Foot IK:相应的Animator State中需要开启Foot IK
- IK Pass:相应的Animator Layer中需要开启IK Pass通道
On Animator IK
动画IK回调函数,Unity Documentation中这样介绍:OnAnimatorIK() 在即将更新其内部反向动力学系统前由动画器组件调用。该回调可用于设置反向动力学目标的位置及其各自的权重。
参数LayerIndex指的是Animator中的Layer层级的索引值。
如何设置IK目标的位置及其权重?需要用到Animator中的函数:
SetIKPosition
:设置一个IK Goal的位置SetIKPositionWeight
:设置IK Goal的过渡权重(0表示IK之前的原始动画,1表示在goal)SetIKRotation
:设置一个IK Goal的旋转SetIKRotationWeight
:设置IK Goal的旋转权重
Calculate IK Position & Rotation
如何获取IK目标位置及旋转?可以通过在脚部加上一定单位的高度上向下进行Raycast
射线检测,RaycastHit
中的point碰撞点即是IK的目标位置,并且通过normal法线方向获得IK的目标旋转,代码如下所示:
#region 计算左脚IK
//左脚坐标
leftFootPosition = animator.GetBoneTransform(HumanBodyBones.LeftFoot).position;
leftFootPosition.y = transform.position.y + raycastOriginHeight;
//左脚 射线检测
leftFootRaycast = Physics.Raycast(leftFootPosition, Vector3.down, out RaycastHit hit, raycastDistance + raycastOriginHeight, layerMask);
if (leftFootRaycast)
{
leftFootIkPosition = leftFootPosition;
leftFootIkPosition.y = hit.point.y + bodyYOffset;
leftFootIkRotation = Quaternion.FromToRotation(transform.up, hit.normal);
#if UNITY_EDITOR
//射线
Debug.DrawLine(leftFootPosition, leftFootPosition + Vector3.down * (raycastDistance + raycastOriginHeight), Color.yellow);
//法线
Debug.DrawLine(hit.point, hit.point + hit.normal * .5f, Color.cyan);
#endif
}
else
{
leftFootIkPosition = Vector3.zero;
}
#endregion
Body Position
在设置IK目标位置之前,需要先计算和调整身体的高度,原因如下图所示,当射线检测到的IK Position,腿的长度达不到时,需要将身体的Y坐标减去相应距离。
身体高度通过Animator中的bodyPosition去调整:
代码如下所示:
#region 身体
if (leftFootRaycast && rightFootRaycast)
{
//左脚坐标Y差值
float leftPosYDelta = leftFootIkPosition.y - transform.position.y;
//右脚坐标Y差值
float rightPosYDelta = rightFootIkPosition.y - transform.position.y;
//身体坐标Y差值取二者最小值
float bodyPosYDelta = Mathf.Min(leftPosYDelta, rightPosYDelta);
//目标身体坐标
Vector3 targetBodyPosition = animator.bodyPosition + Vector3.up * bodyPosYDelta;
//插值运算
targetBodyPosition.y = Mathf.Lerp(lastBodyPositionY, targetBodyPosition.y, bodyPositionLerpSpeed);
//设置身体坐标
animator.bodyPosition = targetBodyPosition;
}
//缓存身体Y坐标
lastBodyPositionY = animator.bodyPosition.y;
#endregion
Apply IK Position & Rotation
求得目标位置和旋转并调整完身体高度后,应用目标位置和旋转即可:
#region 应用左脚IK
//权重
animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, 1f);
animator.SetIKRotationWeight(AvatarIKGoal.LeftFoot, 1f);
Vector3 targetIkPosition = animator.GetIKPosition(AvatarIKGoal.LeftFoot);
if (leftFootRaycast)
{
//转局部坐标
targetIkPosition = transform.InverseTransformPoint(targetIkPosition);
Vector3 world2Local = transform.InverseTransformPoint(leftFootIkPosition);
//插值计算
float y = Mathf.Lerp(lastLeftFootPositionY, world2Local.y, footPositionLerpSpeed);
targetIkPosition.y += y;
lastLeftFootPositionY = y;
//转全局坐标
targetIkPosition = transform.TransformPoint(targetIkPosition);
//当前旋转
Quaternion currRotation = animator.GetIKRotation(AvatarIKGoal.LeftFoot);
//目标旋转
Quaternion nextRotation = leftFootIkRotation * currRotation;
animator.SetIKRotation(AvatarIKGoal.LeftFoot, nextRotation);
}
animator.SetIKPosition(AvatarIKGoal.LeftFoot, targetIkPosition);
#endregion