当前位置: 首页 > news >正文

Unity人形角色IK优化指南

目录

Unity中人形角色的IKI

站立、奔跑IK

1. 接触面法线

2. 调整质心位置

3. 保持原本朝向

攀爬IK

1. 四肢贴合

2. 保持身体与攀爬面的距离

3. 适应外拐角

瞄准IK

1. 头部朝向

2. 手臂朝向

尾声


本文将尝试仅使用Untiy内置的Animator来解决常见的几种运动所需的IK。也会给出核心功能的代码实现。

Unity中人形角色的IKI

逆运动学(Inverse Kinematics,简称IK)在工业机器人领域是指通过末端执行器的位姿来求解相应关节变量的过程。游戏开发中也存在类似需求,即根据末端肢体的位姿来调整身体其他部位的位置。值得庆幸的是,Unity引擎已经帮我们处理了这个复杂的计算过程。

对于使用Avatar的人形角色动画,Unity内置的Animator系统支持调整5个关键部位的IK:头部、左手、右手、左脚和右脚。开发者只需设置好这些部位的IK位置和旋转参数,Unity就会自动计算并调整角色的骨骼运动。

PS:左脚、右脚、左手、右手的IK设置可以通过Animator.SetIKPosition等系列函数,通过AvatarIKGoal的枚举来选择部位;而头部则通过Animator.SetIKPosition等系列函数来控制。

这些函数要在OnAnimatorIK生命周期函数中调用才奏效

站立、奔跑IK

这应该是人形角色最常规的IK了,通常的站立、奔跑、行走等动画都默认是在水平地面上的。但实际游戏地形会复杂很多,我们就需要调节足部的IK来贴合不同的地面。

1. 接触面法线

首先要做的就是通过物理检测找到「落脚点」,简单的射线检测就可以做到,射线检测返回的RaycastHit参数会告诉我们接触点和接触点的法线,以此就可以来调整脚的位置与姿态。

  1. 通过animator.GetBoneTransform得到脚部骨骼的Transform,进而得到脚部骨骼position。从该位置上方一段距离开始,向下检测接触面。人形角色通常是胶囊体,所以迈步时,脚很有可能就超出了胶囊体范围,而脚本身又没有碰撞体,就容易进入碰撞体内部,这时如果只是从脚本身开始检测就会检测失败,所以从上方开始检测。

csharp

/// <summary>
/// 实现类似 pointA.axis = pointB.axis + offset 指定轴向的变化
/// </summary>
private void FoottCheck(HumanBodyBones footBone, int iKGoal_Int, Vector3 upAxis)
{var footPos = animator.GetBoneTransform(footBone).position;//足部上移一段距离后的位置作为射线起点var originPos = footPos + upAxis * upOffset;//检测时指定地面层级遮罩,一般可以忽视触发器if(Physics.Raycast(originPos, -upAxis, out hitInfo, checkRayLength, checkMask, QueryTriggerInteraction.Ignore)){//不直接将足部位置设置为检测到的hit.point//而是将hit.point在upAxis上的分量赋值给足部//相当于把足部沿upAxis方向移到hit.point等高度CalculateAxisValue(ref footPos, hitInfo.point, upAxis);//记录下调整后的足部位置iKGoalPositions[iKGoal_Int] = footPos;//记录下从upAxis到接触面法线所需的旋转iKGoalRotations[iKGoal_Int] = Quaternion.FromToRotation(upAxis, hitInfo.normal);}
}/// <summary>
/// 辅助函数,功能: pointA.axis = pointB.axis + offset
/// </summary>
private void CalculateAxisValue(ref Vector3 pointA, Vector3 pointB, Vector3 axis, float offset = 0)
{pointA += axis * (Vector3.Dot(pointB - pointA, axis) + offset);
}

2. 调整质心位置

光调整脚的位置是不够的,因为这样容易出现一只脚够得着平面,但另一只则 「虚空接触」 的情况(左侧就是没调整的,右侧就是调整后的):

这也是上一步中,要用别扭的方法移动脚部的原因。这样我们就能算得哪只脚触碰接触面所需要移动的距离较大了,我们就将较大的这个偏移量同步应用到animator.bodyPositon就可以了!

csharp

/// <summary>
/// 根据足部在当前up轴的偏差来调整质心位置(身体升降)
/// 为了让奔跑连贯,奔跑时不建议开启,仅静止时开启
/// </summary>
/// <param name="isIdle">是否是闲置状态</param>
private void MoveCentroidPosition(bool isIdle)
{if (isIdle && iKGoalPositions[leftFoot_Idx] != Vector3.zero && iKGoalPositions[rightFoot_Idx] != Vector3.zero && lastCentriodPosInUpAxis != 0) //非闲置时、未获取正确信息时不做调整{var animTransform = animator.transform;//取离躯体最远的脚(更需要贴近地面的脚)与身体的差距作为偏移值var leftOffset = Vector3.Dot(animTransform.up, iKGoalPositions[0] - animTransform.position);var rightOffset = Vector3.Dot(animTransform.up, iKGoalPositions[1] - animTransform.position);finalCentroidOffset = leftOffset < rightOffset ? leftOffset : rightOffset;//在指定方向上线性逼近Vector3 newCentroidPos = animator.bodyPosition + animTransform.up * finalCentroidOffset;float newCentroidPosInUpAxis = Vector3.Dot(animTransform.up, newCentroidPos);//用插值的方式改变质心位置,更自然newCentroidPosInUpAxis = Mathf.Lerp(lastCentriodPosInUpAxis, newCentroidPosInUpAxis, centroidMoveSpeed);CalculateAxisValue(ref newCentroidPos, Vector3.zero, animTransform.up, newCentroidPosInUpAxis);//应用调整后的位置animator.bodyPosition = newCentroidPos; }//将当前质心位置记录为「上次质心在upAxis上的位置」,方便下一帧判断lastCentriodPosInUpAxis = Vector3.Dot(animTransform.up, animator.bodyPosition);
}

你可能注意到了,质心调整并不一定要时时开启,否则像快速上楼梯等斜面变化频繁的情况,可能会剧烈抖动

3. 保持原本朝向

我们希望足部在调整后仍能保持动画原本的偏航角,(也就是说该外八的还是外八,内八的还是内八;而如果像这么做的话,就会导致脚笔直朝向玩家前方:

csharp

iKGoalRot = iKGoalRotations[leftFoot_Int] * animator.transform.rotation;
animator.SetIKRotation(AvatarIKGoal.LeftFoot, iKGoalRot);

显然,问题就出在我们是基于animator.transform.rotation来调整的。所以我们应该在真正调整朝向前,先记录脚部IK原本的朝向,再在记录下的这个朝向上应用步骤1中得到的「贴合地面的旋转」。

csharp

private void MoveFeetToIKPos(AvatarIKGoal iKGoal, int iKGoal_Int)
{//真正调整前,先记录原本IK的位置和朝向var animTransform = animator.transform;var iKGoalPos = animator.GetIKPosition(iKGoal);var iKGoalRot = animator.GetIKRotation(iKGoal);//如果FixedUpdate中没有检测到信息就不更新IKif(iKGoalPositions[iKGoal_Int] != Vector3.zero) {//将当前IKGoal位置和目标IKGoal位置都转到当前坐标系下iKGoalPos = animTransform.InverseTransformPoint(iKGoalPos);iKGoalPositions[iKGoal_Int] = animTransform.InverseTransformPoint(iKGoalPositions[iKGoal_Int]);//从当前坐标的y方向线性逼近目标IKGoal,同样插值逼近显得自然var upVar = Mathf.Lerp(lastPosInUpAxis[iKGoal_Int], iKGoalPositions[iKGoal_Int].y, footIKMoveSpeed);iKGoalPos.y += upVar;lastPosInUpAxis[iKGoal_Int] = upVar;//将调整后的位置转回世界坐标空间(因为SetIKPosition是根据世界坐标的)iKGoalPos = animTransform.TransformPoint(iKGoalPos);//四元数旋转:原本足部旋转的基础上 + 地面贴合旋转iKGoalRot = iKGoalRotations[iKGoal_Int] * iKGoalRot;animator.SetIKRotation(iKGoal, iKGoalRot);}animator.SetIKPosition(iKGoal, iKGoalPos);//清空信息,以待下次FixedUpdate提供信息iKGoalPositions[iKGoal_Int] = Vector3.zero;
}

攀爬IK

通常人形动画的攀爬要调整的是四肢的位置,使其贴合墙面。

攀爬IK的设置方式其实和你所实现攀爬系统的逻辑密切相关,我就暂定现在我们已经实现好了一个攀爬系统,它能时时获取攀爬法线

1. 四肢贴合

最简单的环境,实现思路与足部贴合地面类似,获取四肢IKGaol的位置,然后沿角色后方远离一段距离作为射线检测的起点,往角色的前方进行检测。如下图所示(红色端为射线起点)

csharp

/// <summary>
/// 通过射线检测调整攀爬时四肢IK位置、旋转,并将结果存储在数组中
/// </summary>
private void LimbsClimb_Solver(int iKGoal_Int, LayerMask climbMask)
{var animTransform = animator.transform;//这里假设在攀爬系统的作用下,角色总能面朝攀爬面,故用forwardvar origin = limbsPositions[iKGoal_Int] - animTransform.forward * limbOffset;if(Physics.Raycast(origin, animTransform.forward, out hitInfo, climbRayLength, climbMask, QueryTriggerInteraction.Ignore)){iKGoalPositions[iKGoal_Int] = hitInfo.point;iKGoalRotations[iKGoal_Int] = Quaternion.FromToRotation(animTransform.forward, -hitInfo.normal);return;}
}

「远离一段距离」还有其它好处,比如贴合这种上沿或者内拐角

2. 保持身体与攀爬面的距离

让角色的身体与墙面保持一定距离,可以让动画看起来更顺眼。因为这位置只和墙面有关,所以调整起来也很简单(需要用到攀爬法线climbNormal):

csharp

/// <summary>
/// 调整身体离墙的距离
/// </summary>
private void AdjustBodyPos(Vector3 climbNormal, LayerMask climbMask)
{if(Physics.Raycast(animator.bodyPosition, -climbNormal, out hitInfo, climbCornerRayLength, climbMask, QueryTriggerInteraction.Ignore)){animator.bodyPosition = hitInfo.point + climbNormal * climbDisWithWall;}
}

3. 适应外拐角

有一种比较麻烦的地方是「外拐角」,步骤1中的前向射线检测会扑空。我们需要从两侧向中间检测

具体思路就是四肢向内侧方向进行检测。而且要多段检测,也就是将射线起点向前移动几次,能更好贴合V形角(就算没有刻意的V形墙面,当角色爬过外墙角时也会变成面向V形角的情况)

我们对步骤1中的函数进行补充:

csharp

/// <summary>
/// 通过射线检测调整攀爬时四肢IK位置、旋转,并将结果存储在数组中
/// </summary>
private void LimbsClimb_Solver(int iKGoal_Int, LayerMask climbMask)
{var animTransform = animator.transform;//这里假设在攀爬系统的作用下,角色总能面朝攀爬面,故用forwardvar origin = limbsPositions[iKGoal_Int] - animTransform.forward * limbOffset;if(Physics.Raycast(origin, animTransform.forward, out hitInfo, climbRayLength, climbMask, QueryTriggerInteraction.Ignore)){iKGoalPositions[iKGoal_Int] = hitInfo.point;iKGoalRotations[iKGoal_Int] = Quaternion.FromToRotation(animTransform.forward, -hitInfo.normal);return;}//——————————————————新增部分————————————————else //当前向射线检测不到时,大概率进入了外拐角{//射线起点回到原本位置origin += animTransform.forward * limbOffset;//根据肢体所属左右来设置检测方向var dir = (iKGoal_Int & 1) == 0 ? animTransform.right : -animTransform.right;//向中间进行多次射线检测for(int i = 0; i < cornerRayCount; ++i){if(Physics.Raycast(origin, dir, out hitInfo, climbCornerRayLength, climbMask, QueryTriggerInteraction.Ignore)){iKGoalPositions[iKGoal_Int] = hitInfo.point;iKGoalRotations[iKGoal_Int] = Quaternion.FromToRotation(animTransform.forward, -hitInfo.normal);return;}//如果这次没检测到,就将起点前移origin += cornerRayGap * animTransform.forward;}}
}

瞄准IK

第三人称射击游戏的瞄准,需要让玩家的头能朝向瞄准的地方,玩家拿枪的手也指向瞄准的地方。

1. 头部朝向

头部的处理,我倒是比较简单。因为我的角色会转身,所以头部只需要调整俯仰角就可以了。而头部朝向不一定要百分百朝着瞄准点,看着像个样子就差不多,所以我的选择是——看向手里武器

csharp

public void HeadLookAt(Vector3 weaponPos, float weight)
{animator.SetLookAtPosition(weaponPos);animator.SetLookAtWeight(weight);
}

2. 手臂朝向

调整手臂朝向的一大难点是保持手部姿势,直接设置朝向容易破坏持械姿势。

我的想法是:让双手IK的上下活动限制在一个球面上,这样一来,无论双臂朝向何方手臂伸展的距离都不会变化,这样就能保证动画的姿势维持。

至于这个球心位置,我是简单地选择角色胸骨骼位置,效果还行,动作变形程度不会很大(也可能是因为角色拿着手枪的原因)

csharp

public void BodyLookAt(Vector3 pos)
{//奔跑时胸骨骼会上下移动,瞄准方向会剧烈变化,选bodyPosition来算方向更稳定Vector3 handIKPos, dir = (pos - animator.bodyPosition).normalized;Vector3 chestPos = animator.GetBoneTransform(HumanBodyBones.Chest).position;//双手IK位置调整var handIKGoal = AvatarIKGoal.LeftHand;handIKPos = animator.GetIKPosition(handIKGoal);var originDis = (chestPos - handIKPos).magnitude; //保持半径距离,圆形摆动handIKPos = chestPos + dir * originDis;//奔跑时胸骨骼可能会小幅度上下移动,让手部IK位置也做同样移动animator.SetIKPosition(handIKGoal, handIKPos + animTransform.up * animator.deltaPosition.y);animator.SetIKPositionWeight(handIKGoal, 1);var handIKGoal = AvatarIKGoal.RightHand;handIKPos = animator.GetIKPosition(handIKGoal);var originDis = (chestPos - handIKPos).magnitude; handIKPos = chestPos + dir * originDis;animator.SetIKPosition(handIKGoal, handIKPos + animTransform.up * animator.deltaPosition.y);animator.SetIKPositionWeight(handIKGoal, 1);
}

尾声

还是再次声明一下,这些调整策略都是经验之谈,一定还有更好的调整方式。而且追求更高质量的IK或更多部位IK的调整,可以使用商店插件,或者Unity包里的Animator Rigging。本文就当抛砖引玉了捏!(´▽`)

http://www.dtcms.com/a/330790.html

相关文章:

  • AI搜索优化专家孟庆涛:以技术温度重构“人机信息对话”新范式
  • 手机实时提取SIM卡打电话的信令声音-当前现状与思考
  • CICD-DevOps进阶-2
  • 提升工作效率的利器:GitHub Actions Checkout V5
  • 多种适用于 MCU 固件的 OTA 升级方案
  • Qt基本控件
  • 飞算JavaAI金融风控场景实践:从实时监测到智能决策的全链路安全防护
  • 西门子TIA-FOR循环多路PID控制器(PID_Compact)
  • VirtualBox虚拟机Ubuntu18.04安装hdl_localization保姆级教程
  • 【自动化运维神器Ansible】template模块深度解析:动态配置文件生成的艺术
  • RxJava Android 创建操作符实战:从数据源到Observable
  • 十一,算法-快速排序
  • 大带宽服务器具体是指什么?
  • 十分钟学会一个算法 —— 快速排序
  • 【03】VMware安装麒麟操作系统kylin10sp3
  • Docker运行python项目:使用Docker成功启动FastAPI应用
  • vue3+leaflet案例:告警系统GIS一张图(附源码下载)
  • Mybatis实现页面增删改查
  • 服务器的定义-哈尔滨云前沿
  • [机器学习]07-基于多层感知机的鸢尾花数据集分类
  • Effective Java笔记:要在公有类而非公有域中使用访问方法
  • 解决Maven编译时JAVA_HOME配置错误问题:从报错到根治的完整方案
  • 自动驾驶与人形机器人的技术分水岭
  • springboot博客实战笔记02
  • React.memo、useMemo 和 React.PureComponent的区别
  • 智慧城市SaaS平台/专项管理系统
  • 板子识别出来的所有端点号等信息
  • C++中的链式操作原理与应用(三):专注于异步操作延的C++开源库 continuable
  • 决策树 >> 随机森林
  • 智慧工地从工具叠加到全要素重构的核心引擎