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. Flask API 如何接入 i18n 实现国际化多语言

    ​ 1. 介绍 上一篇文章分享了 Vue3 如何如何接入 i18n 实现国际化多语言,这里继续和大家分享 Flask 后端如何接入 i18n 实现国际化多语言. 用户请求 API 的多语言化其实有两种 ...

  2. 解决方案 | 将时间转换为毫秒bat

    @echo off setlocal enabledelayedexpansion rem 输入的时间 set "time_input=00:07:07.1" rem 解析时间 f ...

  3. Linux安全启动及Machine Owner Key(UEFI BIOS MBR GPT GRUB)

    PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 环境说明   无 前言   只要装过各种系统的人都或多或少会接触 ...

  4. 对比python学julia(第三章:游戏编程)--(第一节)初识游戏库(3)

    1.1.    键盘和鼠标控制 在游戏应用程序中,通常使用键盘和鼠标作为游戏的操作设备.游戏的窗口都能接收来自键盘和鼠标设备的输人.当用户在键盘上按下按建或释放按键时,会产生相应的键盘事件:当用户移动 ...

  5. GeoScene Enterprise 3.1 临时许可更新

    Portal许可更新 portal 的许可更新很简单,直接打开Portal在线更新就好了 平台管理 -> 许可管理 -> 附加许可 -> 导入许可 -> 选择文件(选择授权的j ...

  6. 【ActiveJdbc】02

    一.基本的数据库操作 数据模型层: import org.javalite.activejdbc.Model; 数据访问层: import org.javalite.activejdbc.Base; ...

  7. 【IDEA】找不到类资源

    报错问题描述: 找不到这个实例调用的方法或者方法缺失重载 找不到这个声明的类资源 解决情况一 import声明缺失,IDEA智能导包提示可以解决 注意,如果存在了重名的类资源,导入了错误的资源,实例引 ...

  8. 目前国内全地形能力最强的双足机器人 —— 逐际动力 —— 提出迭代式预训练(Iterative Pre-training)方法的强化学习算法

    相关: https://weibo.com/1255595687/O5k4Aj8l2 该公司对其产品的强化学习训练算法给出了较少的描述: 提出迭代式预训练(Iterative Pre-training ...

  9. 【转载】 拒绝遗忘:高效的动态规划算法 —— “到底什么是动态规划”—— An intro to Algorithms: Dynamic Programming

    原文地址(英文): https://medium.freecodecamp.org/an-intro-to-algorithms-dynamic-programming-dd00873362bb   ...

  10. 《Python数据可视化之matplotlib实践》 源码 第四篇 扩展 第十章

    图 10.1 import matplotlib.pyplot as plt import numpy as np plt.axes([0.1, 0.7, 0.3, 0.3], frameon=Tru ...