动画系统包含:
- 动画片段 Animation Clip,记录物体变化的信息,可以是角色的闪转腾挪,也可以是一扇门的开闭
- 动画状态机 Animator Controller,根据设置切换动画片段
- 动画组件 Animator,Animation
- 替身 Avatar,对人形动画进行复用
动画复用
把一个 .anim 的动画文件作为文本打开
里面有个path属性记录动画要操作的对象的路径,如果根据path找不到对象,这个动画就会失效
其他物体要使用这个动画就必须包含路径一致的对象,否则Animation窗口里就会显示Missing
对于人形动画的fbx文件,选中动画按Ctrl + D就可以把动画复制出来,同样作为文本打开
这里path为空,而attribute记录Avatar中对应的骨骼节点和进行什么操作
Avatar 替身系统
在mixamo上下载一个动画文件
不同DCC软件制作的骨骼节点的名称可能不一样的,不能直接使用该动画,这时候需要借助Avatar建立骨骼和Unity肌肉系统的映射关系
假设A,B两个模型的骨骼名称不同,要把A模型的动画片段A复用到B模型上
步骤:
1.创建AAvatar,AAvatar中保存模型A骨骼和Unity肌肉结构的对应关系,骨骼信息也被保存到Avatar文件中
2.创建BAvatar,BAvatar中保存模型B骨骼和Unity肌肉结构的对应关系
3.通过AAvatar,把动画片段A从描述A的骨骼变化翻译为描述unity肌肉拉伸
4.模型B使用动画片段A,通过BAvatar把动画文件中对Unity肌肉结构的描述翻译为对B模型骨骼变化的描述
具体操作
在模型的Rig面板中选择人形动画,点击Apply
Avatar Definition | Create From This Model 从这个模型本身创建Avatar,把当前模型的骨骼与Unity肌肉结构建立对应关系 |
---|---|
Copy from OtherAvatar 从其他Avatar中复制骨骼层次结构、绑定信息等,需要确保两者具有相似的骨骼层次结构和绑定信息,模型不会创建新的Avatar,只会导入动画 | |
Skin Weights | 蒙皮或者mesh上的节点可以被几个骨骼所影响 |
Strip Bones | 勾选后,Unity会自动删除所有不必要的骨骼,并将相邻的骨骼合并为一个单独的骨骼。这样可以减少骨骼数量和顶点权重数量,从而提高游戏性能 |
Optimize Game Objects | 勾选后会删除模型上的骨骼,从Avatar中读取骨骼信息 |
模型中会出现Avatar,点击Inspector界面上的Configure Avatar进入配置界面
这里可以看到骨骼和Unity肌肉的映射关系,这些Unity基本已经配置好了,一般不需要修改
动画的Rig面板中选择Copy From Other Avatar,并选择来源模型的Avatar,点击Apply
在模型的Animator组件中选择各自的Avatar文件,这时动画就可以复用了
Animator
Apply Root Motion | 有的动画片段自带位移,勾选后就会把位移应用到对象上,如果通过脚本控制位移就不勾选 |
---|---|
Update Mode | Normal 与Monobehaviour的Update同步 |
Animate Physics 与FixedUpdate同步,如果角色动画需要与物理系统交互,选这个 | |
Unscaled Time 与Update同步,忽略TimeScale | |
Culling Mode | Always Animate 不剔除,始终运行动画 |
Cull Update Transforms 不可见时会禁用重定向,ik,Transform变化 | |
Cull Completely 不可见时停止模拟,再次出现时,从停止的状态继续模拟 |
运行时最下面还会显示动画片段相关信息
在Animator中动画状态分为3种:单独的动画片段,多个片段组成的混合树,另一个状态机
在Animator窗口右上角有个Auto Live Link,选中后会在窗口内聚焦当前播放的动画状态
动画片段(AnimationClip)
Tag | 给动画加标签,在代码中就可以根据动画的标签执行不同的逻辑 |
---|---|
Motion | 这个动画状态管理的动画片段,如果是混合树就显示管理的混合树 |
Speed | 动画播放速度,负值就是倒放,脚本无法修改Speed数值 |
Multiplier | 勾选右边的Parameter激活,选择Parameters中的一个浮点型参数关联,动画的速度就等于 Speed * Multiplier |
Motion Time | 范围0~1f,修改后会使动画停在特定时间点,0是开始,1是结束 |
Mirror | 镜像动画 |
Cycle Offset | 播放时的偏移值,0表示不偏移,0.5表示从中间开始播放,偏移不是切割,动画还是会完整播放,只是动画的起始点改变 |
Foot IK | 使用IK矫正,把脚步实际位置向IK Goal位置拉近一点,需要设置IK Goal权重 |
Write Defaults | Animator触发OnEable时,将遍历AnimatorController中的所有片段,收集所有片段的属性值。如果某个片段中没有描述某些属性的变化,是否为其写入默认值 |
正向动力学(Forward Kinematics):常见的动画一般是由骨骼根节点(人形动画是屁股)到末梢骨骼节点依次计算旋转位移缩放来决定每个骨骼的最终位置
反向动力学(Inverse Kinematics):末梢骨骼位置确定,反向计算各个父节点骨骼的旋转位移缩放,比如脚放在台阶上,反向计算其各个父骨骼
使用Avatar把骨骼系统转为肌肉系统后,双手,双脚位置可能出现一些偏移,Unity会保存转换前骨骼系统下手脚的正确位置,并把这些位置放在IK Goal(目标位置)上,也就是上图中双手,双脚位置的红球,手肘处的红球和膝盖处的红球是IK Hint(辅助位置),通过它防止肘部关节出现奇怪的扭曲,Foot Ik参考的是IK Goal的初始位置
使用IK需要在Layer上激活IK Pass,这样就可以在脚本中调用IK相关方法
public class AnimatorTest : MonoBehaviour
{[Range(0, 1)]public float weight;private Animator animator;//动画状态private AnimatorStateInfo stateInfo;//关联Multiplier参数private float animationMultiplier = 1;//Multiplier映射的hash值private int multiplierHash;void Start(){animator = GetComponent<Animator>();multiplierHash = Animator.StringToHash("Multiplier");animator.SetFloat(multiplierHash, animationMultiplier);//获取当前动画状态,0表示base layerstateInfo = animator.GetCurrentAnimatorStateInfo(0);//判断动画的Tag是否为Animif (stateInfo.IsTag("Anim")){//do something}}void Update(){if(Input.GetKeyDown(KeyCode.W)){animationMultiplier += 0.1f;animator.SetFloat(multiplierHash, animationMultiplier);}}/// <summary>/// layer上开启IK Pass/// </summary>private void OnAnimatorIK(int layerIndex){//设置右脚IKGoal的位置animator.SetIKPosition(AvatarIKGoal.RightFoot, new Vector3(1,0,0));//权重越高,右脚越靠近设置的位置animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, weight);}
}
调用时机
当Animator的Update Mode为Nomal或Unscaled Time,OnAnimatorMove和OnAnimatorIk方法与Update同步,当Update Mode为Animate Physics时,与FixedUpdate同步
状态转换(Transition)
两个状态之间可以添加多个转换,这样会变成三个箭头,选中右侧的某个转换就可以为其单独设置转换条件,比如条件1和条件2都会触发 idle -> walk 这个转换,分别设置转换条件即可,它们之间是 “或” 的关系
转换的优先级:
1.如果有勾选了Solo,只执行勾选的转换,不考虑其他的
2.勾选的Solo的转换中,哪个条件先满足就执行哪个转换
3.如果条件同时满足,优先执行上面的转换
4.勾选Mute的转换不会执行
上方可以修改转换名称
Has Exit Time | 勾选表示条件满足也要播放到某个时间点才执行转换,不勾选需要设置Conditions |
---|---|
Exit Time | 比例值,0表示从第一帧转换,0.5表示从中间帧开始 |
Fixed Duration | 勾选表示转换持续时间按秒计算,否则按百分比计算 |
Transition Duration | 转换持续时间 |
Transition Offset | 0表示下一个动画状态从第一帧开始播放,0.5表示下一个动画状态从中间一帧开始播放 |
Transition Duration | 转换持续时间 |
Interruption Source | None 转换不能被打断 |
Current State 可以被其他相同起始状态的转换打断,设置Ordered Interruption需要优先级更高 | |
Next State 可以被其他相同目标状态的转换打断 | |
Current State Then Next State 有相同状态的转换都可以打断,但起始状态一样的优先级更高 | |
Next State Then Current State 有相同状态的转换都可以打断,但目标状态一样的优先级更高 | |
Ordered Interruption | 勾选表示转换按照优先级排序,优先级更高的状态才能打断 |
Conditions | 转换条件 |
状态机里只有四个状态,从A到D。当中的所有转换由相应的触发器变量(trigger)来控制
与A相关的转换有三个
选中A到B的转换,把Interruption Source设置为Current State且Ordered Interruption勾选
当激活触发器来启动A->B的转换时,从A到B的转换就可以被某些同样从A出发的转换所打断了,因为勾选Ordered Interruption,转换只能被优先级更高的A到C的转换打断
Trigger类型的参数只要激活就会触发转换,如果与Trigger相关的动画转换并没有被执行,Trigger会一直处于激活状态,直到转换被执行
Conditions可以设置多条,它们之间是 “与” 的关系,必须同时满足才能触发转换,注意如果勾选Has Exit Time,即使条件满足也得等动画播放到Exit Time才能触发转换
Root Motion动画
Root Motion动画自带根位移,会把动画上的位移应用到对象上,有效避免角色动画和实际位移不同步产生的滑步现象。动画文件会在每一帧里直接修改对象的坐标值和角度值(绝对值),而Root Motion则通过相对位移和转角来移动游戏对象
Animator勾选Apply Root Motion后,Unity会让游戏对象会乘以缩放矩阵,旋转矩阵,平移矩阵
public class AnimatorTest : MonoBehaviour
{private Animator animator;void Start(){animator = GetComponent<Animator>();}/// <summary>/// 使用该方法用代码替代动画修改对象位移旋转/// </summary>private void OnAnimatorMove(){//animator.deltaPosition已经考虑了缩放值transform.position += animator.deltaPosition;transform.rotation *= animator.deltaRotation;}
}
勾选Apply Root Motion并实现OnAnimatorMove,动画由脚本控制
在Generic动画中的使用Root Motion,只需要管理一根根骨骼Root node,实际工作中,美术一般会给模型单独制作一根根骨骼,这个骨骼的作用就是记录模型的位移旋转
Unity会把Generic动画对这根骨骼的操作当作对游戏对象的操作,Apply Root Motion会把根骨骼节点上的绝对坐标和绝对角度,转换为游戏对象的相对位移和相对转角
Root Transform Rotation(绕y轴旋转),Root Transform Position(Y)(y方向位移),Root Transform Position(XZ)(水平方向位移),这三个属性与Root Motion相关的动画
动画文件中的Root Q和Root T表示对游戏对象的旋转和位移,勾选Root Transform Rotation下的Bake Into Pose就不会旋转游戏对象,而是去旋转根骨骼节点,当我们不希望动画带动游戏对象旋转时,就需要勾选这个Bake Into Pose。后面的loop match是检查动画第一帧和最后一帧的吻合度,红色表示不吻合,绿色表示吻合,红色就不要勾选Bake Into Pose
Root Transform Position(Y)下的Bake Into Pose同理,勾选后动画不会影响游戏对象y方向上的位移,而是去修改根骨骼的位移
Root Transform Rotation | 绕y轴旋转 |
---|---|
Bake Into Pose | 勾选表示旋转只影响骨骼和蒙皮(外观),并不影响游戏对象的旋转,能不能勾选参考loop match |
Based Upon | 游戏对象开始时对准的方向 |
Original 动画本来的朝向,美术制作时设置的朝向,一般选这个 | |
Root Node Rotation(Generic) 对准根骨骼节点方向,一般不准确 | |
Body Orientation(Humanoid) 对准上半身前方,一般不准确 | |
Offset | 对旋转做偏移 |
Root Transform Position(Y) | y方向位移 |
Bake Into Pose | 勾选表示y方向位移只影响骨骼和蒙皮(外观),并不影响游戏对象的位置,能不能勾选参考loop match |
Based Upon | 垂直方向上把模型的哪个位置对齐到游戏对象的原点 |
Original 美术在设置的原点,一般选这个 | |
Root Node Rotation(Generic)将根骨骼作为原点 | |
Center of Mass(Humanoid) 质心作为原点 | |
Feet(Humanoid) 脚作为原点,动画复用可能导致Original不准,此时可以选这项 | |
Offset | y方向偏移量 |
Root Transform Position(XZ) | 水平方向位移 |
Bake Into Pose | 勾选表示水平方向位移只影响骨骼和蒙皮(外观),并不影响游戏对象的位置,能不能勾选参考loop match |
Based Upon | 水平方向上把模型的哪个位置对齐到游戏对象的原点 |
Original 美术在设置的原点,一般选这个 | |
Root Node Rotation(Generic)将根骨骼作为原点 | |
Center of Mass(Humanoid) 质心作为原点 |
在动画预览界面点击坐标轴图标就可以显示center of mass,这个质心也被称为body transform,它的位置接近hips骨骼
红色箭头为质心在水平面的投影,我们可以把这个投影当作Root Motion的根骨骼节点,这个点被称为root Transform,代码中这样访问它的位置和方向
private Animator animator;
void Start()
{animator = GetComponent<Animator>();Vector3 bodyTransformPos = animator.bodyPosition;Quaternion bodyTransformRotation = animator.bodyRotation;Vector3 rootTransformPos = animator.rootPosition;Quaternion rootTransformRotation = animator.rootRotation;
}
Humanoid动画中的Root Motion的原理:Unity会计算处一个root transform,Root Motion会把动画文件中描述的root transform的坐标和角度值,转换为相对位移和相对转角,并以此来移动游戏对象
1D混合树
创建混合树,双击进入,点击 “+” 添加三个Root Motion动画
Parameter | 关联一个浮点型参数 |
---|---|
三角形示意图 | 横轴是Speed的值,纵轴是片段的权重,随着Speed的值增大,第一个片段权重减小,第二个片段权重增加 |
Threshold | 参数Speed为多少时,片段的权重为1 |
时钟符号 | 片段播放的速度 |
人形符号 | 是否要镜像动画,仅限人形动画使用 |
Automate Thresholds | 取消勾选 Automate Thresholds就可以修改Threshold的值 |
Compute Thresholds | 根据片段的一些属性重新计算Threshold,需要Root Motion动画 |
Speed 速度的绝对值 | |
Velocity X/Y/Z 分别表示Root Motion在三个方向上的位移速度 | |
Angular Speed(Rad)旋转速度,弧度每秒 | |
Angular Speed(Deg)旋转速度,角度每秒 | |
Adjust Time Scale | Homogeneous Speed 自动计算动画播放速度,使多个动画的移动旋转速度相等,idle动画不需要计算,可排除 |
Reset Time Scale 将所有片段的速度设为1 |
上图中Compute Thresholds选择Velocity Z,即根据前进后退方向上的速度计算Threshold,动画前进的速度大约是1.745667,后退的速度大约是-1.426688。Adjust Time Scale选择Homogeneous Speed,使得动画速度一致
Root Motion的速度不一定是匀速的,这里的1.745667是平均速度
在Animation窗口中观察z方向移动的动画曲线,曲线分成多段并不是线性的
注意:Unity动画做混合时,包含非线性插值计算,无法保证参数Speed为1时,动画速度也为1,这时可以调整播放速度来控制移动速度,把播放速度改成 1/1.745667,这样前进速度就是1
动画中前进后退的速度是针对原本骨骼的,使用Avatar复用动画后会根据骨骼的缩放值调整速度
public class PlayerMoveTest : MonoBehaviour
{private Animator _animator;private float _forwardSpeed = 1.745667f;private float _backwardSpeed = 1.426688f;private float _targetSpeed;private float _currentSpeed;void Start(){_animator = GetComponent<Animator>();//Root Motion会考虑物体的缩放值,humanScale记录了Avatar对骨骼的缩放//我们不希望整个animator的播放速度都受到影响,修改特定动画状态的Multiplier属性_animator.SetFloat("Multiplier", 1 / _animator.humanScale);Debug.Log("humanScale: " + _animator.humanScale);}void Update(){Move();}void Move(){_currentSpeed = Mathf.Lerp(_currentSpeed, _targetSpeed, 0.5f);_animator.SetFloat("Speed", _currentSpeed);Debug.Log("velocity.z: " + _animator.velocity.z);}public void PlayerMove(InputAction.CallbackContext callbackContext){Vector2 movement = callbackContext.ReadValue<Vector2>();_targetSpeed = movement.y > 0 ? _forwardSpeed * movement.y : _backwardSpeed * movement.y;}
}
这里使用了Input System
Root Motion与Rigidbody一起使用
引入Root Motion是为了避免实际位移和动画表现位移不同步,Root Motion解决的是同步问题,而不是位移,要控制位移就需要通过脚本的OnAnimatorMove方法,注意:animator的update mode改为animate physics,动画记得Bake into pose
public class PlayerMoveTest : MonoBehaviour
{private Animator _animator;private Rigidbody _rigidbody;private float _forwardSpeed = 1.745667f;private float _backwardSpeed = 1.426688f;private float _targetSpeed;private float _currentSpeed;void Start(){_animator = GetComponent<Animator>();_rigidbody = GetComponent<Rigidbody>();_animator.SetFloat("Multiplier", 1 / _animator.humanScale);}private void OnAnimatorMove(){Move();}void Move(){_currentSpeed = Mathf.Lerp(_currentSpeed, _targetSpeed, 0.5f);_animator.SetFloat("Speed", _currentSpeed);//物理引擎中会修改rigidbody在y方向上的速度Vector3 vector3 = new Vector3(_animator.velocity.x, _rigidbody.velocity.y, _animator.velocity.z);//用animator从Root Motion动画中提取值,传递给受物理引擎影响的rigidbogy_rigidbody.velocity = vector3;}public void PlayerMove(InputAction.CallbackContext callbackContext){Vector2 movement = callbackContext.ReadValue<Vector2>();_targetSpeed = movement.y > 0 ? _forwardSpeed * movement.y : _backwardSpeed * movement.y;}
}
参考
Unity动画系统详解