Unity中常见人形动画IK的处理方式

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

效果一览:b站视频

Unity中人形角色的IK

IK(inverse kinematics)也就是逆运动学,在工业机器人领域,人们关注的逆运动学问题就是通过末端执行器的位姿来求解对应的关节变量。而在游戏中也类似,我们关注的就是根据末端肢体的位姿来调整身体其它部分的位置,好在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。从该位置上方一段距离开始,向下检测接触面。人形角色通常是胶囊体,所以迈步时,脚很有可能就超出了胶囊体范围,而脚本身又没有碰撞体,就容易进入碰撞体内部,这时如果只是从脚本身开始检测就会检测失败,所以从上方开始检测。


/// <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就可以了!

/// <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. 保持原本朝向

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

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

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

private void MoveFeetToIKPos(AvatarIKGoal iKGoal, int iKGoal_Int)
{
//真正调整前,先记录原本IK的位置和朝向
var animTransform = animator.transform;
var iKGoalPos = animator.GetIKPosition(iKGoal);
var iKGoalRot = animator.GetIKRotation(iKGoal);
//如果FixedUpdate中没有检测到信息就不更新IK
if(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的位置,然后沿角色后方远离一段距离作为射线检测的起点,往角色的前方进行检测。如下图所示(红色端为射线起点)

/// <summary>
/// 通过射线检测调整攀爬时四肢IK位置、旋转,并将结果存储在数组中
/// </summary>
private void LimbsClimb_Solver(int iKGoal_Int, LayerMask climbMask)
{
var animTransform = animator.transform;
//这里假设在攀爬系统的作用下,角色总能面朝攀爬面,故用forward
var 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):

/// <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中的函数进行补充:

/// <summary>
/// 通过射线检测调整攀爬时四肢IK位置、旋转,并将结果存储在数组中
/// </summary>
private void LimbsClimb_Solver(int iKGoal_Int, LayerMask climbMask)
{
var animTransform = animator.transform;
//这里假设在攀爬系统的作用下,角色总能面朝攀爬面,故用forward
var 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. 头部朝向

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

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

2. 手臂朝向

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

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

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

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。本文就当抛砖引玉了捏!(´▽`)

人形动画常见IK的处理的更多相关文章

  1. Unity---动画系统学习(2)---模型3种导入方式、人形动画介绍、切割动画

    1. 介绍 Unity中导入的模型主要是由3DMAX.Maya等建模软件制作的,后缀为.fbx的文件. 博主在Unity Asset Store里面下载了一套官方免费的模型和动画. 和一套地图,分享给 ...

  2. cocos2dx动画常见特效(转)

    本文转载自:http://www.cnblogs.com/linux-ios/archive/2013/04/09/3009292.html bool HelloWorld::init() { // ...

  3. Mecanim之IK动画

    序言:IK动画全名是Inverse Kinematics 意思是逆向动力学,就是子骨骼节点带动父骨骼节点运动. 比如体操运动员,只靠手来带动身体各个部位的移动.手就是子骨骼,身体就是它的父骨骼,这时运 ...

  4. 02、Mecanim之IK动画

    序言:IK动画全名是Inverse Kinematics 意思是逆向动力学,就是子骨骼节点带动父骨骼节点运动. 比如体操运动员,只靠手来带动身体各个部位的移动.手就是子骨骼,身体就是它的父骨骼,这时运 ...

  5. Unity3D学习笔记(十七):IK动画、粒子系统和塔防

    新动画系统: 反向动力学动画(IK功能): 魔兽世界(头部动画),神秘海域(手部动画),人类一败涂地(手部动画) 如何启用(调整) 1.必须是新动画系统Animator 设置头.手.肘的目标点 2.动 ...

  6. Unity 动画

    Unity 并没有自带建模工具. 3D建模工具 maya, 3dmax, blender Skinned Mesh Renderer Mesh Renderer Mesh Filter Modelli ...

  7. Unity动画

    Unity 并没有自带建模工具. 3D建模工具 maya, 3dmax, blender Skinned Mesh Renderer Mesh Renderer Mesh Filter Modelli ...

  8. 关于Unity中Mecanim动画的动画状态代码控制与代码生成动画控制器

    对于多量的.复杂的.有规律的控制器使用代码生成 动画状态代码控制 1:每个动画状态,比如进入状态,离开状态, 等都有可能需要代码来参与和处理,比如,进入这个动画单元后做哪些事情,来开这个动画单元后做哪 ...

  9. [原]Unity3D深入浅出 - 新版动画系统(Mecanim)

    Mecanim概述: Mecanim是Unity提供第一个丰富而复杂的动画系统,提供了: 针对人形角色的简易的工作流和动画创建能力 Retargeting(运动重定向)功能,即把动画从一个角色模型应用 ...

  10. 关于Unity中Mecanim动画的重定向与动画混合

    应用 一个RPG游戏,里面有100种怪物,每种怪物其实都差不多的,行走,跳跃,攻击,难道动画师要调100次动画吗?其实不需要 Unity抽象出人形动画系统,用Unity简化版的骨骼来进行统一的管理,只 ...

随机推荐

  1. Konva 内容重叠无法触发点击事件的解决方法

    写在前面: 环境:Vue3 + Konva + vite 在绘制界面时踩坑,主要是关于 listening 属性的使用 在绘制界面时,不免出现有内容重叠的情况,这会影响事件的触发 使用设置listen ...

  2. C# 线程与进程

    一.前台线程与后台线程对象 为什么要用多线程? 1.让计算机"同时"做多件事情,节约时间. 2.多线程可以让一个程序"同时"处理多个事情. 3.后台运行程序,提 ...

  3. leetcode简单(矩阵):[566, 766, 832, 867, 999, 1030, 1261, 1275, 1337, 1351]

    目录 566. 重塑矩阵 766. 托普利茨矩阵 832. 翻转图像 867. 转置矩阵 999. 可以被一步捕获的棋子数 1030. 距离顺序排列矩阵单元格 1260. 二维网格迁移 1275. 找 ...

  4. [oeasy]python0020换行字符_feed_line_lf_反斜杠n_B语言_安徒生童话

    ​ 换行字符 回忆上次内容 struct包可以让我们使用封包格式 把数字封包到字节里 pack函数负责封包 unpack函数负责解封 我们通过封到不同的字节状态 遍历了一次ascii码 ​ 编辑 还是 ...

  5. SQL Server调用OLE对象

    T-SQL 中是可以调用 OLE 的,将这一功能应用到触发器.存储过程等对象中,SQL Server 运用变得更贴近我们的功能,更加满足我们的需要. T-SQL 中有七个存储过程是围绕本节内容进行的, ...

  6. salesforce零基础学习(一百四十)Record Type在实施过程中的考虑

    本篇参考: salesforce 零基础学习(二十九)Record Types简单介绍 https://help.salesforce.com/s/articleView?id=sf.customiz ...

  7. P10244 String Minimization 题解

    P10244 String Minimization 题意 给你四个长度为 \(n\) 的字符串,分别是 \(abcd\). 你可以选择一个 \(i\) 然后交换 \(a[i]\) 和 \(c[i]\ ...

  8. 【摘译+整理】System.IO.Ports.SerialPort使用注意

    远古的一篇博客,内容散落于博文和评论 https://sparxeng.com/blog/software/must-use-net-system-io-ports-serialport C# 和 . ...

  9. RHCA rh442 006 中断号 缓存命中率 内存概念 大页

    IRQ均衡 硬中断 IRQ是中断号 2003 电脑 拨号 56K Modem USB 打印机 拨号成功,打印机会是乱码,他们会不兼容 因为终端号一样 (类似ip地址冲突) 在bios里面调整设备的中断 ...

  10. ambari2.8+ambari-metrics3.0+bigtop3.2编译、打包、安装

    bigtop编译 资源说明: 软件及代码镜像 开发包镜像 github访问 编译相关知识 技术知识 bigtop编译流程及经验总结 各模块编译难度及大概耗时(纯编译耗时,不包含下载文件和排错时间) c ...