unity 对Animator动画系统的研究
Posted Tearix
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了unity 对Animator动画系统的研究相关的知识,希望对你有一定的参考价值。
unity的新动画系统叫Mecanim,使用Animator来取代旧系统Animation,按Unity文档的惯例:知识点主要分2部分:unity manual和unity script,读者可以边看文章边查阅文档,最好能动手测试。
文章的开始之前,先讲几个基本的知识的:
1.创建动画的一个基本步骤是设置一个unity3d可理解的简化后的骨骼到骨架中实际骨骼的映射;在Mecanim的术语中,这个映射称为Avatar,即avatar是骨骼到骨架的映射。
(图片来自网)
Avatar主要用于类人骨骼模型,可以实现角色之间的Retargeting。非类人模型可以认为骨架就是骨骼。
2.构建模型的基本步骤:
modelling->rigging->skinning(建模->构建骨架->蒙皮)
1.modelling 建模:
1.Observe a sensible topology(遵循合理的拓扑结构),一个合理的标准是动画带动的网格变形是漂亮的;
2.注意网格的缩放比例。最好做一下各个建模软件模型的导入测试,来设置好正确的缩放比例(不同建模软件导入比例不一样)
3.安放角色使得角色的脚站在坐标原点或者模型的“锚点”。角色通常是竖直地走在地面上,如果角色的锚点(也就是他的变换中心)在地面上会更容易控制。
4.如果是类人模型,则尽量使用T字姿态建模(Unity为类人模型提供了许多功能和优化)
5.整理你的模型,去掉垃圾。只要可能的话,覆盖孔洞,焊接顶点并且移除隐藏的面,这会对蒙皮有帮助,特别是自动蒙皮过程。
2.Rigging 搭骨架:创建骨架上的关节来控制你的模型进行运动。
非类人模型的话,可以认为没有骨架,只有骨骼,骨骼直接控制动画,类人模型是骨架控制动画。步骤1中的模型已经有脚,手,头,武器等骨骼,还有受击骨骼等,这些可以用来控制模型或者悬挂额外物件。
3.Skinning 蒙皮:给骨架附加网格。
1.把网格中的顶点绑定到骨骼,包括硬绑定(一个顶点指定一个骨骼,不是一一对应,可能多个顶点指定的是一个骨骼)和软绑定(一个顶点指定多个骨骼,每个骨骼有一定权重)
2.蒙皮的实操步骤:先自动蒙皮,接着用一个测试动画看看蒙皮效果并根据此效果慢慢改啊改。
3.每个顶点最多绑定4个骨骼,这是U3D的上限
3.动画文件导入unity后,我们对它的处理和设置,这些在Inspector面板:Animation Importing settings
分3个页签:
Model:这里的参数基本由美术来定,其他的使用默认就好了,这里只提4个参数:
1.scale factor 模型缩放比例,不同建模软件导入比例不一样,unity的物理单位是1m,这个根据不同建模软件设置
2.readable/writeable和UITexture一样,如果开了unity就必须拷一份到内存,尽量不开。
3.Generate Colliders一般用于生成场景的mesh collider,其他地方就不要打开了。
4.Normals&Tangents是导入时对顶点和切线的处理,比如是否导入模型的顶点等,可限制模型精度和影响后续渲染等,比如vfshader就需要用到模型的 normal。
Rig:这里Animation Type包括(Generic/Humanoid/Legacy/None)
Generic 用于非类人模型;Humanoid用于类人模型
Avatar Definition:可以使用现有的avatar,也可以create from this model,一般地,模型网格文件选create,这个时候,Avatar子资产被增加到模型资产的下面,即Avatar是unity根据网格文件生成的,对Humanoid类型,还会自动匹配骨骼到骨架,不符合会报错;而动画文件使用copy,使用已有资源。
Animations:对一个Animation Clip动画片段进行设置
前面部分的设置这里就不讲了,下面的clip片段我们看到Start和End参数,这表示从一个fbx动画中截取一段给clip使用,多个clip共用一个动画资源,所以,可以一个模型的所有动作都搞到一个动画文件中,各个动作去里面取一段即可;也可以一个模型每个动作都有一个动画文件,分开管理。
中间的参数比较烦躁,略过,先提一下Animations页签最下面的几个有意思的参数:curves,events,mask,这2个简单的参数可以带来许多有趣的功能,在后面会讲到。
讲完上面几个基本知识点后,下面我们来分点看几个有趣的应用:
1.AnimationEvent
Unity manual部分:
这个在animations页签的下面,可以给clip加帧事件,即播到某帧时触发某个事件:
1.给clip的某些帧上加event,这个Function就是事件的名字,其他的是这个函数的参数
2.定义一个脚本来接受这个事件,比如这个图需要定义一个脚本,并且脚本里定义了void ani(xxx){}函数
3.参数的处理:根据脚本的函数定义格式传参数,比如void ani(int a),则传Int那个参数,ani(Object a)则传Object那个参数,ani(float a, string b)则传Float和String这2个参数,而,ani(AnimationnEvent a)则传整个event进去(包括所有参数和当前event对应的clip的信息)
ps:添加了event的clip的animator物体必须挂上定义了该事件名Function的函数的脚本,否则会报错
Unity Script部分:可查看class AnimationEvent,主要是获得该event所在clip的一些信息:与此event相关的stateinfo,clipinfo,和该event本身的信息:event调用的函数名functionName、函数调用的参数:float/int/string/Object(采用哪个看函数定义式)、time(事件的触发时间)
实际应用:这个event机制可以在播到某些帧执行一些事件。那么我们可以,在某个点播某个特效,在某个点播某个声音,在某个点进行一些画面特效,在某个点进行对敌人“击退”“击飞”“击浮空”,非常有利于实现各种节奏效果!还可以加入技能打断机制:当播到某2个帧之间可以被打断:比如我远扑某个玩家,结果我在空中被打断,我就被弹回来。这种效果一定很爽吧。读者可以在一个专门的脚本中定义各种接受事件的函数,并进行相应处理来进行使用此event机制。
2.AnimationCurve:
Unity manual部分:
添加curve,这个curve和event有点像但又不同,event是几帧,curves 则是每帧,curves可以配合OnAnimatorMove使用,比如每帧不同速度前进:
void OnAnimatorMove() { Animator animator = GetComponent<Animator>(); Vector3 newPosition = transform.position; newPosition.z += animator.GetFloat("Runspeed") * Time.deltaTime; //RunSpeed是Curves的曲线变量,控制移动, transform.position = newPosition; //由此类似方法可以让角色一个动画各种移动。 }
官方文档给出一个例子是:
比如人在冰冷环境呼吸时,呼气的水雾由粒子系统控制,那么就可以在播呼吸动画或者站立动画时,通过Animator.Get参数(一个Curve的name)获得当前数值,控制水雾大小。
Unity Script部分:
属性:keys(关键帧key集合),length(the num of keys),postWrapMode(最后一帧循环类型),preWrapMode(第一帧循环类型),this[int]获取关键帧
接口:Evaluate(time):计算某个time时曲线的value
总结:curves感觉就是:边播动画边干点其他事情,event则像播到一些帧就干点事情。2者配合使用能让你的动画系统变得丰富起来,好好使用这两个小小的利器吧。
3.Animation Layers和遮罩 实现:边走边吃苹果
1.给吃苹果动画加遮罩:在animation tab中的Mask中加遮罩,只勾选吃苹果的那部分骨骼,3种情况添加:
A.加现有的遮罩文件,可以通过Assets->Create->Avatar Mast创建一个遮罩
B.如果是给类人模型的动画,使用点击部位加遮罩
C.如果是通用模型的动画,勾选关节加遮罩
2.创建新Layer:EatApple Layer, 把第1步的遮罩拖到这里的Mask参数中,设置该layer设置比走路动画Layer高,并设置该Layer的Blending为override,这样,播走路动画和播吃苹果动画就可以同时进行并且播苹果动画override了走路动画的上半身动画。
4.Animator Override Controller:
顾名思义,它就是Override “Animator Controller”的,先简单说一下Animations Controller:
Animation Controller可以认为它就是动画状态机, Animator动画系统是通过Animator Controller来控制动画播放的,里面存着指向各个动画片段Animation Clip的引用,和播放动画的逻辑,比如状态转移等.
而Animator Override Controller则用于拓展一个已存在的Animtor Controller,它只是在后者基础上在某些状态播新的动画Clip而已,保持后者的状态机逻辑,结构等等:retaining the original’s structure, parameters and logic.
所以在应用方面,Animator Override Controller能做到一个状态机(Animator Controller)实现多套动作,非常利于维护。按官方说法就是:
如果一类模型可以共用动画状态机,则可以弄一个基础Animtor Controller,里面是基本的动画状态逻辑,然后给某个模型使用就弄一个Animtor Override Controller,然后把其中不同的动画Clip换成自己的,这样就只需维护一份动画状态机了,省心省力。比如NPC系统的各个NPC。许多怪物也可以共用,主角更不用说。
5.2种换装系统
(复习一个知识点:Skinning 蒙皮:给骨架附加网格,把网格中的顶点绑定到骨骼)
先加载2个模型出来,第1个模型是基础模型,skinmesh/动画/Animator等都有,第2个模型则是一个带skinmesh的几乎空的模型,第一个是源模型,第二个是目标模型,我们现在要做的就是用第2个模型的skinmesh替换掉第1个模型的skinmesh,做法如下:
a.获得第一个模型的skinmesh old_meshrender,获得第2个模型的skinmesh dst_meshrender
b.用新的sm的信息更新旧sm信息
SkinnedMeshRender dst_meshR = newModel.GetComponent<SkinnedMeshRender>(); SkinnedMeshRender old_meshR = oldModel.GetComponent<SkinnedMeshRender>(); Transform[] bones = old_meshR.bones; Transform[] newBones = new Transform[dst_meshR.bones.Length]; for (int i = 0; i < dst_meshR.bones.Length;++i) { for (int j = 0; j < old_meshR.bones.Length; ++j) { if (old_meshR.bones[j].name == dst_meshR.bones[i].name) { newBones[i] = old_meshR.bones[j]; break; } } } old_meshR.sharedMesh = dst_meshR.sharedMesh; old_meshR.bones = newBones; old_meshR.sharedMaterials = dst_meshR.sharedMaterials;
Destroy(newModel);
这种换皮技术其实做了两件事:
1.使用新skinmeshrender的内容(mesh and material)更新旧skinmeshrender的内容;
2.更新旧sm绑定的骨骼集。第2步是因为新sm的骨骼集和旧的不同,一般情况下不变,需要注意,新sm虽然存了骨骼,但我们只用它的骨骼集信息(可以认为是所有骨骼名)来更新旧sm,又因为使用的是sharemesh和sharemetarial,所以最后可以直接destory掉已经无用的新sm资源了。
这种换皮只是更新皮肤信息,还有一种换皮是换掉skinmeshrender:
1.Init():加载裸模(只有animator和骨骼transform集) 2.初始化皮肤也走换皮流程 3.换皮流程: a.如果裸模root下挂了旧的sm,destroy掉它 b.新加载出来的sm为新sm,把它挂到裸模root下,然后根据新sm的骨骼集信息,从裸模的骨骼集里收集到对应的骨骼集,赋值: 新sm.bones = bones[list of transform from 裸模骨骼集] 最后把新sm的骨骼集都删了: Destroy(新sm.rootBone)
这种换皮就是换掉sm了。
两种换皮方式都挺好的。
6.混合树
状态转移和混合树,虽然两者都是用来制作平滑动画,但区别是大大的(读者可只看前3点即可):
1.状态转移是从一个动画状态平滑转移到另外一个动画状态,2个状态的动画可以区别很大也表现得漂亮,不能维持中间状态,即不能产生新的动画让你播;是动画状态机的一部分,是一个过程。
2.动画混合树是把多个动画混合起来:
blend multiple animations smoothly by incorporating parts of them all to varying degrees
将角色不同的部分混合,对一个部分而言,是混合所有动画中该部分的角度,形成该部分的新角度,是混合动画。
有参数控制各个动画的混合权重,以达到目的(比如左转:一开始是向前的权重大,向左的权重小,然后。。。)
要使混合效果好,各个动画需要造型类似,这个和转移不同;动画混合树作为一种特殊的状态存在在动画状态机中,是一个状态,新的状态。
3.鉴于上述描写,制作混合树的子动画需要注意:
Examples of similar motions could be various walk and run animations. In order for the blend to work well, the movements in the clips must take place at the same points in normalized time. For example, walking and running animations can be aligned so that the moments of contact of foot to the floor take place at the same points in normalized time (e.g. the left foot hits at 0.0 and the right foot at 0.5). Since normalized time is used, it doesn’t matter if the clips are of different length.
我的理解是,走路动画和奔跑动画都在0.7的时候右脚触地,不然0.7的时刻融合后,右脚在半空,也就是说动作在一些关键点比如离地和触地。在时间线上的百分比应该相近的,在此基础上,各个动作的Length不用考虑,比如run就比walk短,这无所谓的。
4.混合树的创建,选一个State->create blend tree,双击blend tree进入混合树编辑页面,右边Inspector中就是控制这个混合树混合的anim clips和参数。
5.一些参数:有一个Compute Thresholds的参数,可以帮忙指定各种阈值控制混合。比如X方向的速度等。
6.给混合树添加已创建的参数:在Parameter选择栏里手动键入,回车!这个同给动画状态机加参数是一样的
7.上面的4是针对一维混合,二维混合一般用不到,不讨论,多维混合蛮有趣的,可以用于控制脸部表情,多维混合的做法(那控制脸部表情做例子):给出多个表情动画,每个动画对应一个参数控制混合的权重,这样通过控制各个参数可以“生产”出很多有意思的表情动画了。
8.之前一直不知道怎么移动浏览Animator 窗口的各自状态:Alt+鼠标左键!
9.animator的TargetMatching技术
假如你有一个跳跃的动画,要实现跳跃到某个物体上的效果,可以考虑用animator的TargetMatching技术:
get到模型的Animator ani,然后
animator.MatchTarget(jumpTarget.position, jumpTarget.rotation, AvatarTarget.LeftFoot, new MatchTargetWeightMask(Vector3.one, 1f), 0.141f, 0.78f);
0.14,和0.78是起跳和跳完所在时间百分比
注:我测试没成功,没看到跳到某个物体的效果,=。=,或许哪步出错了。
10.IK控制骨骼
步骤:
1.模型的Rig的Animation Type选Humanoid,动画状态机controller的Layer勾上IK Pass;
2.写个脚本挂到模型预制上,打开void OnAnimatorIK()函数,在这个函数里控制IK:
具体看Manual的InverseKinematics.html:控制模型转向和各个部位的Rotation和Position
注:已测试,效果还有趣,可能在一些有意思的地方用到,或许项目想做一系列可控的动画会用到吧,否则,直接制作动画就解决了。
11.根运动
根运动是动画本身自带的,比如行走动画,如果不apply root motion则“原地踏步”(in-place),如果应用则向前行走。
实际项目中一般,不apply root motion,而通过代码来设置transform postion,总结而言:
1.不apply root motion并且该animator组件所在obj上的控制脚本不实现OnAnimatorMove,且所有脚本也不控制该模型position,=>模型原地踏步;
2.不apply root motion并且该animator组件所在obj上的控制脚本不实现OnAnimatorMove,但其他脚本控制该模型position,=>模型不原地踏步;
3.animator组件所在obj上的控制脚本实现OnAnimatorMove(这个时候apply root motion变不可选了),且所有脚本也不控制该模型position,=>模型不原地踏步,又OnAnimatorMove控制;
4.animator组件所在obj上的控制脚本实现OnAnimatorMove(这个时候apply root motion变不可选了),且其他脚本控制该模型position,=>模型不原地踏步,即受其他脚本控制又受OnAnimatorMove控制;
5.apply root motion(由4可知不能挂带OnAnimatorMove的脚本了),且所有脚本也不控制该模型position,=>模型不原地踏步,按动画里根动作移动;
6.apply root motion(由4可知不能挂带OnAnimatorMove的脚本了),且其他脚本控制该模型position,=>模型不原地踏步,按动画里根动作移动的同时也受其他脚本控制;
PS1:上述只提了位置,根运动也包括rotation,且控制逻辑也是同样的。
官方说明:Root motion is the effect where an object\'s entire mesh moves away from its starting point but that motion is created by the animation itself rather than by changing the Transform position. Note that applyRootMotion has no effect when the script implements a MonoBehaviour.OnAnimatorMove function.Changing the value of applyRootMotion at runtime will re-initialize the animator.
11.一些优化建议
1.The Animator doesn’t spend time processing when a Controller is not set to it
2.少用缩放动画曲线
3.When importing Humanoid animation use a BodyMask to remove IK Goals or fingers animation if they are not needed.
如果是类人动画,用身体遮罩剔除IK Goals和不需要的细节动画(如手指动画)
4.When you use Generic, using root motion is more expensive than not using it. If your animations don’t use root motion, make sure that you have no root bone selected.
如果选通用模式,不要使用root motion,不要选择root bone
5.Use hashes instead of strings to query the Animator.用Hashid而不用string来获取动画状态或参数,动画的Play函数重载了hash和string,可以考虑做一个映射,string->hash,使用hash获取参数(播动画),来代替string.
6.使用curves来实现一些额外表现:在播动画的时候同时搞事情。
7.Rig页签的optimize gameobjects(选create from this model才可用)
12.Animator Component
Unity Manual:参考这篇:http://www.cnblogs.com/Tearix/p/6941156.html
Unity Script:属性和接口太多了,看官方文档吧,有个翻译可以参考一下,但那个只是作者自己的理解,不保证准确:
http://www.cnblogs.com/hont/p/5100472.html?utm_source=tuicool&utm_medium=referral
13.Animator大概是如何串起来的:
Animator anim = xx; AnimatorController controller = anim.runtimeAnimatorController; //每个controller可能有多个layer,不过一般只有一个layer:BaseLayer AnimatorControllerLayer layer = controller.layers[0]; //每个layer有个状态机 AnimatorStateMachine sm = layer.stateMachine; //状态机里有许多状态 //默认状态 AnimatorState defaultState = sm.defaultState; //状态列表 ChildAnimatorState[] states = sm.states; //真正的状态 AnimatorState state = states[i].state; //子状态机列表 ChildAnimatorStateMachine[] stateMachines = sm.stateMachines; //状态是动画状态机的基本模块了,每个状态里包含一个动作(Motion, this is a clip or blendtree(遗弃)),当处于此状态时播此动作,当过度到一个新状态时,先变成那个状态,然后动作再慢慢切过去,这里有一个动作序列的概念。 //这个是状态内的动作 Motion motion = states[0].state.motion; //blendtree遗弃后,motion就是AnimationClip了; AnimationClip clip = motion as AnimationClip; //clip里面还有AnimationEvent,帧率,是否类人动作,wrapMode,clip长度等变量和AddEvent, SetCurve等函数接口。
14.一个坑
Play(A);Play(B);则最终Play了A;以前好像遇到过有概率不播某个动画的情况,没查明是否是因此造成的。解决方法是:
Play(A);Update(x);Play(B);则最终Play了B,用Update更新一下就可以覆盖了。如果是CrossFade,情况又复杂一些了,这个问题我专门和一位网友讨论过,这里不提。
以上是关于unity 对Animator动画系统的研究的主要内容,如果未能解决你的问题,请参考以下文章
Unity3D之Mecanim动画系统学习笔记:Animator Controller
Unity 动画系统 Animation 和 Animator的小实例