参考
Unity动画系统详解7:Layer是什么?
学习笔记 --- Unity动画系统
“大智,我的Animator状态机里面的状态很多,现在太难管理了。我有很多姿势,比如不拿枪,拿手枪,拿冲锋枪,拿步枪,每种姿势都要做一个混合树,现在再去加蹲的状态,简直爆炸,有没有什么好办法?”
“那你想想这些动画之间有没有什么相似的地方可以提取出来的?”
“要说相似的话,它们腿部的动画是类似的,不管拿枪姿势如何,腿部都是一样的,只有胳膊的动画不太一样。”
“嗯,这你就算说到点子上了!这时候可以用下Animator Controller中的Layer
有了Layer,我感觉我的动画可以分为上半身和下半身两个Layer,这样一来就不需要对每个拿枪的动作设置移动的混合树了,应该能简化不少工作。”
“确实是这样,合理利用Layer不仅可以减少动画师的动画制作工作量,还能在性能上对游戏进行优化。”
使用Layer可以用来管理角色的不同身体部位。比如下半身用于行走或跑步,上半身用于射击或投掷物体。
使用动画分层的应用主要是以下情况
- 人物动画状态涉及分部并行逻辑,例如枪战游戏,腿部动作由键盘控制,而上半身动作需要由鼠标控制进行IK瞄准,以及武器切换
- 存在状态的融合/叠加效果,例如在正常走动的基础上,搬运物品时,上半身需要持握物品并进行IK绑定,下半身仍是正常走动
- 动作游戏中,为减轻动画师工作量,取用不同动作的部分进行组合
一、Layer
1.管理Layer
点击加号可以添加一个Layer。点击Layer旁边的齿轮图标,会弹出一个小窗口,可以设置该Layer对应的参数。
- Weight 这一层的权重,0代表该层权重是0(该层不生效),1代表该层权重为1(该层中的动画能完全表现)。混合权重值可以通过代码进行动态获取、修改,例如我们根据人物的HP值,逐渐将正常走动效果,转为受伤的走动效果
animator.GetLayerWeight(layerIndex);
animator.SetLayerWeight(layerIndex, weight);
- Mask 设置该Layer能控制身体的哪部分,设置后该Layer上面会显示一个M的小图标。详细笔记见下方
- Blending 和其他层混合的模式。
- Override 覆盖上面的层中对应Mask的部位
- Additive 会加在之前Layer的动画之上。
- Sync 复用其他层中的状态机。详细笔记见下方
- IK Pass 什么是IK?得问问大智
二、Avatar Mask
Avatar Mask,替身遮罩体,可以作为一种资源文件在Assert目录下创建
用来设置该Layer的动画会影响角色的哪一部分的遮罩。AvatarMask有两种定义身体遮罩的方式:
- 通过Humanoid身体映射图
- 通过Transform层次结构选择包含或不包含的骨骼
1.Humanoid
如果你的动画是人形动画,建议用这一种方式可以快速设置AvatarMask。
身体映射图包含以下几部分:
- 头部
- 左臂
- 右臂
- 左手
- 右手
- 左腿
- 右腿
- Root(脚下的圆形阴影部分)
可以通过鼠标点击每一部分,绿色代表动画可以影响这一部分,红色代表动画不会影响。在空白处点击可以全选/全不选。手部和脚部的IK可以开关,表示该部分的IK曲线是否参与动画混合。IK又出现了,一会去问问大智
2.Transform
如果动画没有使用Humanoid,或者你想更精细的控制遮罩,你可以通过Transform层级结构来控制每一个骨骼。
- 选择一个Avatar(动画模型的Avatar)
- 点击Import Skeleton按钮。avatar的层级结构会显示出来。
- 可以设置对应的骨骼是否受动画控制
3.Animation页签中设置Mask
在模型导入设置的Animation页签中,也可以设置Mask,设置后只会导入对应Mask的动画数据。
在这设置Mask的好处是:可以减少内存占用和CPU占用。因为不需要的身体部位的动画曲线不会被加载,并且不会参与计算。
三、混合示例一
我们需要实现这样的一个效果,人物能够进行正常的奔跑站立,只不过在我们摁下空格的时候,无论何种姿态,我们都希望人物能够挥舞自己的右手,而其它姿态保持原状态
1.Base Layer
2.New Layer
在下层实现一个从空状态(不引用动画切片),到Wave动作的过渡,配置层遮罩限定右手,混合模式为Overrid绝对坐标重写
3.运行
运行动画,下层空状态时不对上层产生影响。当下层过渡为Wave挥手动作时,由于层遮罩的配置,只重写右手部分的动作,因此人物右手产生挥手动作,其它部位仍保持Base Layer的效果。
四、混合示例二
我们需要在人物原有的正常站立奔跑动作上叠加一层摆臂效果(或者其它什么效果)
1.BaseLayer和之前一样,实现人物的走动和奔跑
2.下层动画使用Addicted叠加模式,这里我们不使用层Mask,而是用ClipMask实现
3.运行效果
当下层为摆臂动作时,由于ClipMask的配置,仅双臂部分的动作被叠加到了上层。这个摆臂的动作其实是从正常的跑步动画中得到的,但由于ClipMask的配置,仅双臂部分保留了跑动的摆臂动作,其它未激活节点被恒置于Default状态。
在进行Addicted,相对运动叠加混合时,双臂部分保留的动作就被叠加了上去,而其它部分由于恒置于Default状态没有相对运动,就不会产生影响。
这里其实通过层Mask配置,而不使用ClipMask,也可以实现,并且有些人也是这么去做的。注意!!!这两种做法其实都没有问题,针对这个简例也没有孰优孰劣之分。但我这里就是要借故去提一下ClipMask,去用一下这种做法,告诉你ClipMask可以这样用,以免参与到一些网课老师以及博主的,冷落ClipMask的队伍中去。
五、Sync 复用其他层中的状态机
有时能够在不同的Layer重用其他Layer的状态机非常有用。比如你想模拟一个“受伤”的状态,你有受伤状态的各种动画比如走跑跳。这时候你可以选中Sync复选框,选择你想要同步的Layer。状态机的结构会保持一致,但是可以设置不同的Animation Clip。选中时,Layer旁边会有一个S小图标。
这意味着,同步出来的Layer不需要再去定义状态机,并且源Layer中状态机的任何变化都会同步到这一同步Layer。唯一需要你做的就是设置每个State中使用的动画。
Sync复选框旁边还有一个Timing复选框。
- 不选中时,Sync出来的Layer中每一个State的动画长度会变为源Layer中的时长。
- 选中时,根据Weight调整动画时长,Weight为1时使用Sync Layer中的动画时长。
镜像层与源层的状态,动画组,过渡设置是被绑定在一起的,修改一方会同时影响另一方。在游戏运行时,镜像层会与源层保持状态的同步(按照其中一方Clip的时间为标注,对另一方的时间进行控制)。但不同的是,镜像层可以在动画状态中,引用与源层不同的动画片段。从而我们可以通过镜像层,对一些同步执行,动作不一,可复用状态配置的分层动作可以通过镜像层进行快速的创建。例如对之前的挥手效果,我们创建一个镜像层,引用挥舞另一只手的动画片段。
六、IK
参考
Unity动画系统详解8:IK是什么?
学习笔记---3dMax动画系统(机械、角色动画篇)
1.概念
大智,我看完了,这个东西还是很强大的呀,正好符合我的需要。不过这里面有好多地方提到了IK,这个IK是什么东西呢?”
“IK是Inverse Kinematics的缩写,也就是反向动力学。”
“那是不是还有正向动力学?”
“没错,你都会举一反三了!大多数动画是通过旋转骨骼来实现的。子骨骼的位置跟着父骨骼的旋转而改变,因此关节链的终点可以根据前面的各个骨骼的角度和相对位置确定。这种构成骨骼动画的方法称为正向动力学。”
“那反向动力学是不是就是根据骨骼的最终节点,反向推算之前的骨骼节点的位置?”
“哟,不错哦,有长进。正如你所说的一样。有些时候我们需要根据空间中的位置来确定骨骼节点的位置,比如让角色拿枪,不同的枪可能握持的位置不太相同,就需要根据握持的位置来决定角色手的位置。Unity中的IK支持所有人形动画。”
其他的用途其实还有比如:角色的头的旋转,这样可以和你视角的方向一致。角色的脚的位置,这样可以让角色踩在地面更贴合。”Unity中IK能设置的部位就是5个,分别是:头、左右手、左右脚。所以没有其他部位的IK了,我们常见的其实也都是这些。
“这样说我就有些明白了,Animator中的State设置中的Foot IK是不是就是设置脚部受IK的影响?”
“是的。”
“那Layer中的IK Pass是什么意思呢?”
“选中IK Pass的时候,每帧会调用脚本中的OnAnimatorIK方法,可以在这个方法中动态设置IK。
2.如果对反向动力学还是不太理解,可以参考Inverse Kinematics 逆向运动学
逆向动力学的过程在一些场景下十分有用,例如一个机械臂,你需要它去抓取一个放置在特定的空间位置的物体,那么就需要利用逆向动力学去计算出机械臂各个关节的旋转角度,进而驱动机械臂去抓取物体。此外,在游戏编程以及CG动画制作等领域,逆向运动学也扮演这非常重要的角色。
为了使得以上的描述更加形象,这里举一个2D逆向运动学的例子:
给定两段长度固定的刚性关节,并固定住了一个关节作为起点,最后给出一个固定的目标位置(target)。此时只能对两个关节进行任意角度的旋转,来让关节的末端达到给定的目标位置,下面的图例中给出了无解,唯一解,两个解的情形。
但是,某些解决方案将比其他解决方案更好。 例如,若结构代表一个动画人物的手臂,则某些解看起来会更舒适和自然,而另一些解则会显得僵硬及不合理。 所以通常会有一个最佳解决方案。
3.设置头部IK
“那Layer中的IK Pass是什么意思呢?”
“选中IK Pass的时候,每帧会调用脚本中的OnAnimatorIK方法,可以在这个方法中动态设置IK
大智:“我们先来看看如何设置人物的头部根据视角旋转。需要用到这两个API:
public void SetLookAtPosition(Vector3 lookAtPosition);
“这个方法用来设置头部看向的位置,比如看向你左边的窗户,头就会相应的旋转。”
“这个看起来很简单嘛。”
“对,这个方法确实很简单,不过还有另外一个:”
public void SetLookAtWeight(float weight, float bodyWeight = 0.0f,
float headWeight = 1.0f, float eyesWeight = 0.0f, float clampWeight = 0.5f);
“这个方法用来设置IK的权重,这个IK会和原来的动画进行混合。如果权重为1,则完全用IK的位置旋转;如果权重为0,则完全用原来动画中的位置和旋转。至少要设置第一个参数,后面的几个参数都有默认值,但是你也要了解所有参数的含义:”
- Weight 全局权重,后面所有参数的系数
- bodyWeight 身体权重,身体参与LookAt的程度,一般是0
- headWeight 头部权重,头部参与LookAt的权重,一般是1
- eyesWeight 眼睛权重,眼睛参与LookAt的权重,一般是0(一般没有眼睛部分的骨骼)
- clampWeight 权重的限制。0代表没有限制(脖子可能看起来和断了一样),1代表完全限制(头几乎不会动,像是固定住了)。0.5代表可能范围的一半(180度)。
大智:“有了这两个方法你就可以实现头部的IK了,不过还有两点需要注意:”
- 需要勾选对应Layer的IK Pass选项(在Layer的设置里)。
- 代码需要写在OnAnimatorIK这个事件方法里面。
void OnAnimatorIK(int layerIndex)
{
_animator.SetLookAtPosition(pos);
_animator.SetLookAtWeight(1);
}
上面的代码就是人物的头部看向一个位置的代码。需要注意的是这个OnAnimatorIK方法有一个参数layerIndex,这个就是对应的Layer的序号,只有勾选了IK Pass的layer才会调用到这个方法里,每个勾选了IK Pass的layer调用一次。
小新:“这样我就能实现人物的头跟着视角移动了,哦也”
大智:“是的哦”
4.设置手脚IK
小新:“那手脚的IK是不是也跟这个类似的?”
大智:“是的,手脚的IK是和这个类似的,不过API有些不一样,我们来看看”
public void SetIKPosition(AvatarIKGoal goal, Vector3 goalPosition);
public void SetIKRotation(AvatarIKGoal goal, Quaternion goalRotation);
设置头部时,因为头不会移动,所以只需要设置LookAt的位置,头部跟随旋转即可。
但是对于手和脚,需要同时设置位置和旋转。
goal AvatarIKGoal枚举类型,包含:
- LeftFoot 左脚
- RightFoot 右脚
- LeftHand 左手
- RightHand 右手
goalPosition/goalRotation IK目标位置/旋转
同样还有设置权重的API:
public void SetIKPositionWeight(AvatarIKGoal goal, float value);
public void SetIKRotationWeight(AvatarIKGoal goal, float value);
goal AvatarIKGoal枚举类型
value IK的权重,1代表完全使用IK值,0代表使用原动画的值
常见的设置手部IK的代码是(一般需要4行代码设置一个部位):
void OnAnimatorIK(int layerIndex)
{
_animator.SetIKPosition(AvatarIKGoal.LeftHand, position);
_animator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1);
_animator.SetIKRotation(AvatarIKGoal.LeftHand, rotation);
_animator.SetIKRotationWeight(AvatarIKGoal.LeftHand, 1);
}
人物左手与球体的绑定示例:
public Transform IKBall_1;//绑定目标
public Vector3 IKMove_1;//偏移量
private void OnAnimatorIK(int layerIndex)
{
if (layerIndex == 0)
{
if (animator.GetCurrentAnimatorStateInfo(0).shortNameHash == idleHash)
{
animator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1);
animator.SetIKPosition(AvatarIKGoal.LeftHand,IKBall_1.position +
transform.forward * IKMove_1.z + transform.right * IKMove_1.x + transform.up * IKMove_1.y);
animator.SetIKRotationWeight(AvatarIKGoal.LeftHand, 1);
animator.SetIKRotation(AvatarIKGoal.LeftHand,
Quaternion.LookRotation(transform.forward, -transform.right));
}
}
}
5.IK位置/旋转调节小技巧
小新:“大智,这个IK的位置好难调整啊,我想让角色拿枪的手能够贴合这个枪,有没有什么简单的办法?我这调了一个多小时了,还不是特别完美。。。”
大智:“调IK是个慢活,不过呢,确实有一些小技巧在里面。IK相关的代码涉及到位置和旋转,这时候不要傻傻的直接定义一个位置和旋转来手动设置,最好的办法是设置两个参照物,作为IK的位置和旋转的参考,这样只需要调这两个参照物就可以了。”
小新:“对对对,这样的话就不用去修改位置和旋转的值,而是直接修改这俩参照物的位置和旋转就可以了。我来试一下。”
小新:“太棒了,这样我就能在运行时调整这个参考位置,调到一个完美的位置和角度。”
小新三下五除二,就调到了一个合适的位置和角度。
“调好了!”小新高兴地喊道,随即退出了Play状态。
大智:“高兴早了吧?你这么就退出来了,修改的能保存下来么?” 记好了,点击Transform组件右上角的小图标,可以Copy Component,在运行时点击,退出运行后,再点击小图标,选择Paste Component Values,这样就可以将数据粘贴回来了。”
6.关节绑定
关节绑定是在使用四肢绑定,产生了人物手/脚部对目标位置绑定的基础上,对人物的肘关节/膝关节进行位置绑定。注意!!!必须在使用了对应的四肢绑定,才能令关节绑定生效,不能单独进行关节绑定(必须绑定了左手位置,才能对左臂肘关节进行绑定)
关节绑定的方法:
animator.SetIKHintPositionWeight(AvatarIKHint, Weight);
animator.SetIKHintPosition(AvatarIKHint, Position);
其中AvatarIKHint是一个枚举类型,包括人物的左右肘关节,左右膝关节
通常我们会根据四肢绑定的目标位置,结合人物的Transform物体坐标,按一定的算法/偏移量去推算关节绑定的位置。例如下面的示例代码,让人物踩在圆球上并保证小腿竖直:
public Transform IKBall_1;//目标球体
public Vector3 IKMove_1;//右脚绑定偏移量
public float kneeMove;//膝关节偏移量
private void OnAnimatorIK(int layerIndex)
{
if (layerIndex == 0)
{
if (animator.GetCurrentAnimatorStateInfo(0).shortNameHash == idleHash)
{
animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, 1);
animator.SetIKPosition(AvatarIKGoal.RightFoot, IKBall_1.position +
transform.forward * IKMove_1.z + transform.right * IKMove_1.x + transform.up * IKMove_1.y);
animator.SetIKRotationWeight(AvatarIKGoal.RightFoot, 1);
animator.SetIKRotation(AvatarIKGoal.RightFoot, Quaternion.LookRotation(transform.forward, Vector3.up));
animator.SetIKHintPositionWeight(AvatarIKHint.RightKnee, 1);
animator.SetIKHintPosition(AvatarIKHint.RightKnee, IKBall_1.position +
transform.up * (IKMove_1.y + kneeMove) + transform.forward * IKMove_1.z + transform.right * IKMove_1.x);
}
}
}
7.IK的相互影响
如果IK方法与方法之间会产生相互影响,那么它们不能被置于同一个层中。
相互影响是指一方的IK绑定,会导致另一方需要绑定的位置有所改变。这是由于进行IK绑定的瞬间,IK效果并不会立即被体现出来,导致一方对另一方绑定位置的影响具有滞后性,导致后绑定的一方绑定位置出错。
如果你分别启用两端IK绑定,效果无误,而在同一层中同时启用时出错,那么就可以断定是相互影响的滞后性导致的。此时我们可以创建一个空的动画层并勾选IK Pass用于运行IK逻辑,在OnAnimatorIK中使用逻辑分支在不同的层中运行两段逻辑。
例如人物的注视+手部绑定,由于绑定位置是头部下方的一个节点,导致同时启用时出错
此时我们可以创建一个空层开启IKPass,并在Animator中对不同的分层分别使用两端绑定逻辑
if (animator.GetCurrentAnimatorStateInfo(0).shortNameHash == idleHash)
{
if (layerIndex == 0)
{
//注视绑定
animator.SetLookAtWeight(1, 1, 1, 1, 1);
animator.SetLookAtPosition(Aim.position);
}
else if (layerIndex == 1)
{
//双手绑定
animator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1);
animator.SetIKPosition(AvatarIKGoal.LeftHand, IKBall_1.position);
animator.SetIKRotationWeight(AvatarIKGoal.LeftHand, 1);
animator.SetIKRotation(AvatarIKGoal.LeftHand,
Quaternion.LookRotation(transform.forward, transform.TransformDirection(new Vector3(-1f, -1.73f, 0f))));
animator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1);
animator.SetIKPosition(AvatarIKGoal.RightHand, IKBall_2.position);
animator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1);
animator.SetIKRotation(AvatarIKGoal.RightHand, Quaternion.LookRotation(transform.up, transform.right));
}
}