动画的实现方式
-
在Unity引擎中使用Animator系列制作动画,Unity编辑器提供了一系列动画制作工具
-
使用外部工具3dmax、maya制作动画,导入到Unity
-
使用代码直接修改GameObject的Transform实现动画,通常使用协程实现。布娃娃系统是使用代码实现动画的典型例子。使用代码控制动画的好处是能够实现最大的灵活性,例如让一个小球在某个半径内随机运动,如果使用关键帧动画就无法做到完全随机。
本文只介绍在Unity引擎中制作动画。
Unity旧版动画系统
Unity是一个不断发展的游戏引擎,它的很多模块都在与时俱进。
-
输入系统从InputManager到InputSystem
-
UI系统从Unity UI到Element新版UI
-
基础架构从面向对象到DOTS
Unity的动画系统也有两套,旧版animation动画系统和新版的Animator动画系统,其中Animator动画系统也叫Mecanim动画系统。因为旧版的动画系统叫animation,新版的动画系统只能找一个动画的同义词,也就是mecanim。旧版的animation比新版的动画系统简单很多,个人认为不该删掉,两者并存更好。
旧版的Animation组件直接持有一个Animation对象,新版的Animator持有一个AnimateController。
Macanim动画系统的特点
-
面向动画应用的动画系统,使用3dmax、maya可以导出fbx文件,unity中直接导入动画文件
-
基于状态机的动画控制系统
-
核心是关键帧动画。
Mecanim最初与人形角色动画密切相关,后来经过扩充可以适用于其它动画。
动画相关的组件
-
Animate:也叫AnimationClip,动画本身,是一种资源文件,后缀名为.animate,像视频一样。这种资源有两种来源:1. 使用Animation窗口进行编辑;2.从外部导入动画。
-
AnimateController:一个有限状态自动机,持有若干个Animate。AnimateController也是一种资源文件,后缀名为.controller.可以设置进行状态转移的条件。
-
Animator:MonoBehavior,一个GameObject需要持有一个Animator才能播放动画。Animator依赖AnimateController。
Animator持有AnimateController,AnimateController是一个状态机,每个结点都是Animate动画。AnimateController和Animate都是资源文件。
动画与Playables与Timeline的关系
Playables API是动画系统的底层接口,Animator是基于Playables API的封装。
AnimateController底层实现基于PlayableGraph,所以可以使用状态机描述。
-
Playables API 允许动态动画混合。这意味着对象在场景 可以提供自己的动画。例如,武器、箱子和陷阱的动画可以动态添加到 PlayableGraph 并使用一定的持续时间。
-
Playables API 允许您轻松播放单个动画,而无需创建和管理 AnimatorController 资产所涉及的开销。
-
Playables API 允许用户动态创建混合图并直接逐帧控制混合权重。
-
PlayableGraph 可以在运行时创建,根据需要添加可播放节点。与启用和禁用节点的巨大“一刀切”图不同,PlayableGraph 可以进行定制以适应当前情况的要求。
Animation窗口
选择GameObject,然后添加属性,创建关键帧。
有两种模式:
-
录制模式,可以录制物体的属性变化
-
关键帧模式,手动创建关键帧,直接编辑属性
在底部,Animation窗口有两种视图,一个是Dopesheet(关键帧),另一个是Curbes(曲线模式)。可以使用曲线编辑器编辑动画。
AnimationController窗口
Entry为入口
Exit为出口
Entry会默认连接一个结点,这个默认结点就是最初的动画,这条线为橙色。可以右键结点,选择Set as Layer default State.
Any State:任意状态,是一个始终存在的特殊状态。
SubState Machine:子状态机,相当于包含多个结点的一个子状态机,它是为了便于整理状态。
Solo和Mute功能,这个功能主要是用于测试,使用Solo仅启用勾选了Solo的动画过渡。
为什么需要AnyState?例如角色有一个死亡状态,从跑步、吃饭、运动三个状态都可以到达死亡状态,那么需要画三条线:跑步-死亡,吃饭-死亡,运动-死亡。有了AnyState只需要画一条线,AnyState-死亡。
AnimationController中的每一个结点都有一些属性:
-
Motion:结点对应的动画
-
Speed:动画播放速度
-
Multiplier:用于平滑播放动画
-
Mirror:仅适用于人形动画,表示是否使用动画的镜像
-
Cycle Offset:动画其实偏移量,取值0到1
-
Foot IK:人形动画是否启用脚部IK
-
Transision:该状态向其它状态的转移列表,其中的Solo和Mute两个参数用于调试。开启Solo之后表示该State只能进行这一种过渡,其它过渡暂时关闭;Mute表示这个过渡暂时关闭,只能进行其它过渡。
-
AddBehaviour:用于向状态机添加行为
Animator
ApplyRootMotion:是否应用根对象的位移。勾选后, 在动画播放期间, 物体的运动相关参数完全由动画本身接管, 此时脚本控制无效. 取消勾选后, 则是由脚本来控制物体的运动参数。脚本中实现了OnAnimatorMove,相当于勾选了ApplyRootMotion,可以在脚本中控制位置和旋转。
deltaPosition:相对上一帧的位置变化量(必须允许根运动才能被计算)
deltaRotation:相对上一帧的角度变化量(必须允许根运动才能被计算)
void OnAnimatorMove()
{
var transform1 = transform;
transform1.position += animator.deltaPosition;
transform1.rotation *= animator.deltaRotation;
}
CullingMode:剔除模式,可用于提升性能。取值如下:
-
Always Animate:始终渲染
-
Cull Update Transforms:当物体不被摄像机可见时,仅计算根节点的位移,只保证位置正确
-
Cull Completely:当物体不可见时,完全停止动画。
骨骼-蒙皮动画
人类的很多研究都是针对人类本身进行的,例如人体骨骼动画,人类面部识别。
游戏中有很多角色,每个角色有自己的样式。
一个角色包括:骨骼、蒙皮、动画。角色的动画系统非常复杂,可能是多个骨骼同时运动。
动画控制的是骨骼的关键帧,蒙皮跟着动,角色就动起来了。
Animation的时序
-
ProcessAnimations 读取骨骼信息
-
FireAnimationEventsAndBehaviours 读取动画事件
-
ApplyOnAnimatorMove 根节点应用动画信息
-
WriteAnimatedValues 动作数值写入
-
DirtySceneObjects 对骨骼的transform进行更新写入
-
MeshSkinning.CalcMatrices 计算蒙皮矩阵
-
ScheduleGeometryJobs 子线程处理
-
MeshSkinning.Skin 计算模型,网格顶点位置
-
MeshSkinning.Render 渲染
-
PutGeometryJobFench 几何计算
-
Mesh.DrawVBO DrawCall调用
-
角色模型性能优化:
-
导入人形动画时,如果不需要IK,使用Avatar遮罩将其移除
-
取消掉Update When Offscreen,在不可见时不用更新动画。
Unity外部资源
使用其它软件制作复杂模型,然后导入Unity,这是一种常见的工作方式。
Blender、AutoDesk、Maya是三种比较常见的建模软件。
fbx文件是一种常见的3D文件格式。
动画复用:Avatar化身系统
在制作骨骼蒙皮动画的时候,可以为骨骼设置动画。能否将一套骨骼动画应用于两个不同的角色?可以,这就是Avatar化身系统,它的作用是将一套动画应用于多个模型。
分层和遮罩
动画分层意思是把两个动画进行组合,例如上半身动画和下半身动画分开播放。
Animator API
GetCurrentAnimatorStateInfo
GetNextAnimatorStateInfo
animator.Play("动画状态名称")
animator.Update(duration) 将动画时间更新到duration之后
animatorController结点的动画重写:
Animator animator = GetComponent<Animator>();
AnimatorOverrideController overrideController = new AnimatorOverrideController();
overrideController.runtimeAnimatorController = animator.runtimeAnimatorController;
overrideController["name"] = newAnimationClip;
animator.runtimeAnimatorController = overrideController;
检查动画的状态
//检查是否正在播放jump动画.
AnimatorStateInfo stateinfo = anim.GetCurrentAnimatorStateInfo(0);
bool playingJump = stateinfo.IsName("jump");
if(playingJump)
{
if(stateinfo.normalizedTime < 1.0f)
{
//正在播放
}
else
{
//播放结束
}
}
为AnimatorController中的每个结点添加脚本
AnimatorController的每个状态都可以挂载脚本,只需要继承StateMachineBehaviour
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class JumpState : StateMachineBehaviour
{
private GameObject player;
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// 正在played的状态的第一帧被调用
Debug.Log("------OnStateEnter------------");
}
// OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
}
// OnStateExit is called when a transition ends and the state machine finishes evaluating this state
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// 转换到另一个状态的最后一帧 被调用
Debug.Log("-------------OnStateExit-----------------");
}
// OnStateMove is called right after Animator.OnAnimatorMove()
override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// 在OnAnimatorMove之前被调用
}
// OnStateIK is called right after Animator.OnAnimatorIK()
override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// 在OnAnimatorIK之后调用,用于在播放状态时的每一帧的monobehavior。
// 需要注意的是,OnStateIK只有在状态位于具有IK pass的层上时才会被调用。
// 默认情况下,图层没有IK通道,所以这个函数不会被调用
// 关于IK的使用,可以看看这篇文章《Animator使用IK实现头部及身体跟随》
// https://www.jianshu.com/p/ae6d65563efa
}
}
使用代码控制播放速度
Animator ator = go1.GetComponent<Animator>();
var stateinfo = ator.GetCurrentAnimatorStateInfo(0);
if(stateinfo.IsName("Jump"))
{
ator.speed = 2;
}
动画事件
动画事件用于通知GameObject当前正在播放何种动画,可以携带float、int、string、object四个参数。
Animator IK
在动画中应用逆向骨骼。
MatchTarget
当人的手抓住某个位置时,人的手位置固定,动画继续播放。
例如,扣篮的时候手与篮筐相对静止,身体绕着篮筐旋转。
public Animator ani;
public Transform LeftHand;
bool hasJump = false;
void Start () {
ani = GetComponent<Animator>();
}
void Update () {
if (ani)
{
AnimatorStateInfo info = ani.GetCurrentAnimatorStateInfo(0);
if (Input.GetKeyDown(KeyCode.Space))
{
ani.SetBool("Jump", true);
}
if (info.IsName("Base Layer.Vault"))
{
ani.SetBool("Jump", false);
// 第一个参数动作位置,第二个参数角色旋转,第三个是做动作的某个身体部位,第四个是权重信息,第五六参数是获取动画曲线
ani.MatchTarget(LeftHand.position, LeftHand.rotation, AvatarTarget.LeftFoot, new MatchTargetWeightMask(new Vector3(1, 1, 1), 0), ani.GetFloat("StartA"), ani.GetFloat("EndA"));
hasJump = true;
}
}
}
}
使用代码根据fbx创建状态机
using System.Collections;
using UnityEditor;
using UnityEditor.Animations;//5.0改变 UnityEditorInternal;并不能用了。
public class CreateAnimatorController : Editor
{
[MenuItem("ModelConfig/创建Controller")]
static void DoCreateAnimationAssets()
{
//创建Controller
AnimatorController animatorController = AnimatorController.CreateAnimatorControllerAtPath("Assets/animation.controller");
//得到它的Layer
AnimatorControllerLayer layer = animatorController.layers[0];
//将动画保存到 AnimatorController中
AddStateTransition("Assets/Art Resources/Character/moster-002/basic/moster-002@run.FBX", layer);
AddStateTransition("Assets/Art Resources/Character/moster-002/basic/moster-002@stand.FBX", layer);
AddStateTransition("Assets/Art Resources/Character/moster-002/basic/moster-002@born.FBX", layer);
}
private static void AddStateTransition(string path, AnimatorControllerLayer layer)
{
AnimatorStateMachine sm = layer.stateMachine;
//根据动画文件读取它的AnimationClip对象
AnimationClip newClip = AssetDatabase.LoadAssetAtPath(path, typeof(AnimationClip)) as AnimationClip;
////取出动画名子 添加到state里面
AnimatorState state = sm.AddState(newClip.name);
//5.0改变
state.motion = newClip;
Debug.Log(state.motion);
//把state添加在layer里面
AnimatorStateTransition trans = sm.AddAnyStateTransition(state);
}
}
参考资料
https://zhuanlan.zhihu.com/p/492136094
https://www.jb51.net/article/221837.htm
Unity官方文档:https://docs.unity3d.com/cn/2021.3/Manual/AnimationOverview.html