学习目标:
学习内容:
学习时间:
学习产出:
我们可能会卡住,点击mixamo回到主页面然后再进来就不会卡了
1.首先我们得知道Mixamo是什么?
Adobe旗下免费的动画开源网站
2.其次,我们需要注册一个Adobe账户
3.选择喜欢的动画
我们这里下载三个动画,Idle,Walking,Run
选择自己喜欢的动画点击下载,这里的模型推荐是去实验X或者Y机器人,因为他们的模型动画比较精细和大众化。
四个选项,
第一个Format,选择For Unity
第二个Skin,我们携带一个skin,skin就是模型,如果选择none,那么只会下载一个Animation
第三个Frames per second 表示一秒多少帧,显然帧数越高越好,但是为了节省性能和内存还是选择30就行了
第四个KeyFrameReduction 表示是否选择忽略微小动作,也是节省性能的一个选项,如果你勾选了。
那么当animation导致模型发生细小的变化,可能会被忽略掉这一帧,以节省内存。所以这里不勾选,选择None。
然后我们选择下载Walking,这里与idle不同的是,我们的动画是让人往前移动的。那么,播放这个动画,人物就会往前移动,这就会脱离我们的控制,所以我们只需要他的动作,不需要他去影响人物的Transform,所以这里勾选Inplacer让他保持不动
因为我们在Idle已经下载了skin,所以这里不需要选择skin了。
下载完成导入
我们发现这里就idle有模型,其他的没有,也验证了勾选skin的效果
4.介绍Animator
我们需要设置模型的Animation进入Editor,然后勾选looptime,即循环播放。
三个同样的做法。
然后我们选择动画,按ctrl+d即可分动画出来。
放置idle模型进入场景,然后带上animator组件。我们创建AnimatorController,然后将其赋值给animator,然后将三个动画放入,注意,这里也要查看
在animator里面的animation是否已经勾选了looptime。
然后我们介绍Animator组件里面的三个状态
首选是Entry,表示进入的时候播放默认的动画,也就是黄色的动画,这个我们可以自己设置。
其次是AnyState,也就是任何状态,我们可以在程序运行的任意时间去控制Animator播放动画,也就是通过这个AnyState,前提是你这个动画提前与AnyState存在连接。
然后就是Exit,也就是退出的时候播放的动画,这个在角色死亡等时候用的比较多。
在Animator里面都是通过连线去从一个动画播放到另外一个动画
里面有个Exittime属性,也就是是否包含退出时间,如果勾选上了,那么只有当当前动画播放完成了才会进入下一个动画,如果没有勾选Exittime,那么就会直接过渡。
然后我们仔细研究Animator组件
第一个Controller,就是控制这个物体的Animator
第二个Avatar,这是什么呢?因为人物模型是个非常类似的模型,每个人物模型的骨骼都类似,所以Unity就设置了这个属性。比如头,胳膊,腿等,唯一不同的可能是骨骼的数量不同。所以Unity为我们建立一套标准的骨骼,我们需要把自己的骨骼映射到标准骨骼中,这样我们就可以实现人形动画的重用(不同人物的动画通用)。
第三个Apply Root Motion,Animator.ApplyRootMotion
绑定该组件的GameObject的位置是否可以由动画进行改变(如果存在改变位移的动画)。
这个属性是用来控制物体在播放骨骼动画的时候是否应用骨骼根节点的运动参数。
一、当没有骨骼根节点的情况时,比如只是一个Cube立方体,如果勾选了ApplyRootMotion,运行后不会播放动画,因为应用了骨骼根节点的运动参数,而没有骨骼根节点,就没有动画了。即便是在代码中强行调用Animator.Play(“rotation”)方法也不会播放动画。
二、当有骨骼根节点的情况时,一旦设置了这个变量为true,那么请一定注意,这个会对物理引擎在模拟对象的运动轨迹时产生直接的影响,例如在某个动画A中,对象只向Y轴方向进行了移动,在X和Z轴是静止的,那么我们在播放A动画的时候,如果使用Rigidbody设置速度或者施加外力,还是不会让物体在X和Z轴上发生位移的。这是因为,在整个动画播放的过程中(例如0.5秒),Animator会根据动画中物体的位移信息对物体的速度进行赋值,这样达到使用骨骼根节点的位移的效果,也就是说,我在播放动画过程中的任意时间给物体设置了X或者Z轴方向上的运动速度,后续的动画播放帧中,速度又会被Animator强制赋值为跟动画文件中的位移信息一致。
通俗的说就是会让你动画影响模型的Transform
第四个UpdateMode,如果是Normal,动画的播放会和TimeScale保持一致,TimeScale为0.5,动画就慢一半。如果是Animate Physics,那么动画就会和物理组件扯上关系。如果是UnscaledTime那么就代表我们的动画是独立的,不会和TimeScale发生联系。
第五个CullingMode,如果是Always Animate,字如其名,不管摄像机拍不拍,都会进行播放更新。如果是Call Update Transforms表示摄像机看不见时停止动画播放但是位置会继续更新。如果CullCompletely当摄像机不再拍摄该角色的时候,动画暂停,然后拍摄到了就会继续,就像没有暂停过一样。
5.Animator详解
这里我们需要注意,当我们的模型设置为Humanoid的时候。Create Avatar from this model的时候。我们从Mixamo下载的可不仅仅只是动画了,我们需要把skin下载下来,才能进行Avatar的绑定,才能给人形模型共用你的动画
首先,在Animator组件的左边,有Parameters参数。这里面有一个参数集合,也就是当前Animator的参数集合。参数有四个类型。
Float 浮点型,带有小数点
Int 整型
Bool 代表True or False
Trigger 类似于Bool 但是有一点点不同 翻译为“触发器”
创建一个Bool
正如前面所述,在Animator里面动画过渡是依靠Transition
这里可以定义Transition的名字,还有ExitTime是否勾选,不勾选的话,当我们满足转换条件就可以立即转换,不需要等动画播放完成。如果勾选了,那么ExitTime为0.97就是代表当当前动画播放到百分之97才会在最后百分之3进行平滑过渡。
然后我们在这里添加了条件,只有IsWalking为True的时候,我们才能进行转换
同时,我们还有其他参数可以进行操作
比如说Solo和Mute
Solo简单的来说就是可以单独用,比如说Walking同时连接了Walk和Run两个Transition,那么,只要Run这个Transition设置为了Solo,那么Waliking只会往Run转换,不会Walk。如果Walking连接了三个Transition,其中有两个为Solo,那么就会在这两个里面根据条件选择。
同样的Mute,忽略一个。如果Mute了Run,那么只会Walk不会Run。
第一个参数TransitionDuration也就是转换比例,看下面对蓝色三角形,边占比就是整个动画的百分之25。也就是代表着,动画播放到百分之75就开始切换,达到一个平滑过渡。
第二个
TransitionOffset为0就是从第二个动画0秒开始计算,如果存在Offset就会偏移。比如偏移为0.1,那么第二个动画的前百分之10就不会进入计算。
InterruptionSource 大神讲解 大神讲解2 大神讲解3
Animator animator;
int isWalkingHash; //更简单的存储方式 Hash值
int isRunningHash; //更简单的存储方式 Hash值
// Start is called before the first frame update
void Start()
{
animator = GetComponent<Animator>();
isWalkingHash = Animator.StringToHash("IsWalking");//将Hash值代表IsWalking
isRunningHash = Animator.StringToHash("IsRunning");//将Hash值代表IsWalking
}
// Update is called once per frame
void Update()
{
bool isWalking = animator.GetBool(isWalkingHash); //节省性能 我们在Bool值是目标值的时候,就没必要一直赋值了
bool isRunning = animator.GetBool("IsRunning");
bool forwardPressed = Input.GetKey(KeyCode.W); //节省代码
bool ShiftPressed = Input.GetKey(KeyCode.LeftShift);
if (!isWalking&&forwardPressed)
{
animator.SetBool(isWalkingHash, true);
}
if (isWalking && !forwardPressed)
{
animator.SetBool(isWalkingHash, false);
}
if (!isRunning&&(forwardPressed && ShiftPressed))
{
animator.SetBool(isRunningHash, true);
}
if (isRunning&&(!forwardPressed ||!ShiftPressed))
{
animator.SetBool(isRunningHash, false);
}
}
这段代码对性能考虑很充分,可以仔细研究。
比如,通过Hash ID进行引用,Animator.StringToHash把字符串转换成Hash ID。通过“Hash ID”更有优势,因为不容易出错,且更加高效。
还有利用Bool值去判断是否要持续赋值。可以节约性能,避免每一帧去赋值一次。
6.Animator的BlendTree
大神讲解
首先我们得知道BlendTree是什么,他就是一个动画混合器。我们可以通过Transition的方式去转移动画,为什么还需要BlendTree呢?因为,BlendTree的混合能力更强大。
设想一下,主角移动有八个方向,上下左右。那么你改怎么去设计Transition好让动画完美匹配呢?很难,但还是可以。
但是,如果使用BlendTree可以轻松完成。
首先,我们从1D开始。
这里面融合了两个动画,基本的讲解我就不说了,有很多教程可以学习。我们就来分析一下这里面的一些关键点。
首先,Parameter就是参数,也就是BlendTree是根据什么来进行融合的。
Motion也就是你的动画,Threshold就是融合的参数。
下面有一个AutomateThresholds,我们勾选就代表我们是自动生成。其实还能根据动画的Speed或者XYZ等一系列属性去融合,但是这里,我们就根据Velocity就行了。
然后就是
也就是TimeScale,我们可以自己去设定动画速度。
同时也有选项让我们去选择,**Adjust Time Scale > Homogeneous Speed ** 可以将动画的速度调整对应到参数的最小值和最大值,但是保持动画的初始相对速度。先将所有动画的平均速度算出来,然后通过调节动画的speed让所有动画的速度都一致。
Reset就是重新设为1。
有个问题
自动计算Threshold的时候,那些动画的速度啊,旋转速度怎么知道啊?
我们选中一个Animation Clip,你可以看到这些数据。
其次是2D
2D Simple Directional(2D简单方向):当你的运动代表不同的方向,如“向前走”,“向后走”,“向左走”,“向右走”,或“向上瞄准”,“向下瞄准”,“左瞄“和”右瞄“。当然了,可以在(0,0)处包含一个默认动作类似“空闲站立”或“直线瞄准”。与1D混合树不同的是,2D Simple Directional不是在同一个方向上的多个动作,比如“走”和“跑”。
2D Freeform Directional(2D自由方向):动画运用有不同的方向时,也可以使用这种混合类型:可以在同一个方向上有多个运动,例如“走”和“跑”。在Freeform
Directional类型中,(0,0)位置必须包含一个默认动作,如“空闲站立”。2D Freeform Cartesian(2D自由笛卡儿):当混合的2个参数不代表不同的方向时使用。使用Freeform Cartesian,参数X和Y可以表示不同的概念类型,例如角速度和线速度。举个例子:“向前走不转向”,“向前跑不转向”,“向前走并右转”,“向前跑并右转”等动作。
Animator animator;
float VelocityZ = 0.0f;
float VelocityX = 0.0f;
public float acceleration = 2.0f;
public float deceleration = 2.0f;
public float maximumWalkVelocity = 0.5f;
public float maximumRunVelocity = 2.0f;
int VelocityZHash;
int VelocityXHash;
// Start is called before the first frame update
void Start()
{
animator = GetComponent<Animator>();
VelocityXHash = Animator.StringToHash("VelocityX");
VelocityZHash = Animator.StringToHash("VelocityZ");
}
void ChangeVelocity(bool forwardPressed,bool leftPressed, bool rightPressed, bool runPressed, float currentMaxVelocity)
{
if (forwardPressed && VelocityZ < currentMaxVelocity)
{
VelocityZ += Time.deltaTime * acceleration;
}
if (leftPressed && VelocityX > -currentMaxVelocity)
{
VelocityX -= Time.deltaTime * acceleration;
}
if (rightPressed && VelocityX < currentMaxVelocity)
{
VelocityX += Time.deltaTime * acceleration;
}
}
void LockVelocity(bool forwardPressed, bool leftPressed, bool rightPressed, bool runPressed, float currentMaxVelocity)
{
if (!forwardPressed && VelocityZ > 0.0f)
{
VelocityZ -= Time.deltaTime * deceleration;
}
if (!forwardPressed && VelocityZ < 0.0f)
{
VelocityZ = 0;
}
if (!leftPressed && VelocityX < 0.0f)
{
VelocityX += Time.deltaTime * deceleration;
}
if (!rightPressed && VelocityX > 0.0f)
{
VelocityX -= Time.deltaTime * deceleration;
}
if (!leftPressed && !rightPressed && VelocityX != 0.0f && (VelocityX > -0.05f && VelocityX < 0.05f))
{
VelocityX = 0.0f;
}
//lock forward
if (forwardPressed && runPressed && VelocityZ > currentMaxVelocity)
{
VelocityZ = currentMaxVelocity;
}
//decelerate to the maximum walk velocity
else if (forwardPressed && VelocityZ > currentMaxVelocity)
{
VelocityZ -= Time.deltaTime * deceleration;
if (VelocityZ > currentMaxVelocity && VelocityZ < (currentMaxVelocity + 0.05f))
{
VelocityZ = currentMaxVelocity;
}
}
//round to the currentMaxVelocity if with in offset
else if (forwardPressed && VelocityZ < currentMaxVelocity && VelocityZ > (currentMaxVelocity - 0.05f))
{
VelocityZ = currentMaxVelocity;
}
}
// Update is called once per frame
void Update()
{
bool forwardPressed = Input.GetKey(KeyCode.W);
bool leftPressed = Input.GetKey(KeyCode.A);
bool rightPressed = Input.GetKey(KeyCode.D);
bool runPressed = Input.GetKey(KeyCode.LeftShift);
float currentMaxVelocity = runPressed ? maximumRunVelocity : maximumWalkVelocity;
ChangeVelocity(forwardPressed, leftPressed, rightPressed, runPressed, currentMaxVelocity);
LockVelocity(forwardPressed, leftPressed, rightPressed, runPressed, currentMaxVelocity);
animator.SetFloat(VelocityXHash, VelocityX);
animator.SetFloat(VelocityZHash, VelocityZ);
}
7.让动画播放在其他模型上
要做到这一点,我们需要骨骼绑定。但是Unity自带的Avatar就可以帮我们完成这件事了,因为里面有一个Human属性,当我们勾选上,自动绑定,然后我们就可以共用人形动画。
我们下载模型动画的时候就需要带上skin了
然后进入Unity
我们就会发现生成了一个这样的东西。这就是Avatar。然后这个动画,就可以给所有人形模型使用了。
8.Animation Layer 层的概念
我们在FPS游戏设计案例(我的另一篇文章)里面已经用到过AnimationLayer的概念。就是层的覆盖。
当我们层的权重比较大的时候,就会覆盖下面一层,让动画播放上层的。
那么,我们就需要了解Mask(遮罩)的概念了。他会取消动画对身体某一部分的影响。
如图,如果我们将这个Mask赋值给对应的层,那么这个层就不会对红色区域产生影响,只会对绿色区域产生影响。这样就可以实现动画的分离。里面的Use skeleton from表示,传入一个类人的Avatar,比如说有翅膀和尾巴,但是用的比较少。默认设置为None。
这就融合了瞄准和走两个动画。
同时Animation有Curve属性
当走在不平整的路上,我们希望人物的高度是随着变化的,具体会在以后进行介绍。
Layer 显示M表示有Mask,显示A表示勾选additive,
我们这里有五个参数,
第一个Weight也就是权重,影响比例。
第二个Mask,前面介绍过。
第三个Blending混合模式,如果为Override则会覆盖上一层的信息。
如果为additive。
在Animation中,我们是会存储每一个骨骼的变化数据。additive意为混合所有标记为additive的层。权重越高,他就越重要,影响也就越大。
但是,如果覆盖层和附加层的上身动画不一样会怎么样?结果是会发生变化。
因为每一个动画都具有所谓的“附加参考姿势”,默认情况下,这是动画的第0帧,但是可以在fbx动画处和当前动画的Debug面板进行修改。
在我们理解了additive的原理之后,我们可以来设计一个蹲下的操作。
蹲下的第0帧就已经是蹲下了,如果直接让他们进行混合,就会变得突兀。
我们设置两个层,一个是Idle,一个是Crouch层。那么当我们设置其为additive,权重为1,发现人物在原地鬼畜的慢慢动。
和我们想象的不一样。
因为这里Crouch的第0帧就是蹲下的,而Idle一直就是站着的。所以他们的骨骼节点的Transform组合之后,位置就会变得不一样了。
那么,我们修改Crouch动画,让Idle动画的第0帧赋值给Crouch的第0帧。
我们播放Crouch动画,发现会突然闪下去。因为每一帧间隔时间太短了,无法达到融合的效果。
后面的帧都是在重复,所以为了节约性能都可以删掉
当我们再次融合的时候,神奇的事情发生了
居然additive融合成功了。虽然Crouch动画只有两帧,但是实现了蹲下的同时也存在idle的呼吸动作。
这就是additive融合。
然后就是Sync参数了
当我们勾选Sync之后,网格布局就会改变,产生一个名为SourceLayer的选项,默认选择我们的BaseLayer。
同时这一层里面的动画等都会变成BaseLayer层的。意思就是Crouch层将BaseLayer层进行同步了。如果这个时候在BaseLayer层进行修改的话,Crouch层同样会进行同步修改。
那这有什么用呢?
让我们设计一个简单的BaseLayer层,这里有idle-walk-run。
那么我们设计一个健康系统,让主角的健康状态通过动画的方式直接展示给玩家。那么我们就可以将这个层Sync我们的BaseLayer层,将里面的idle-walk-run都替换成受伤状态下的idle-walk-run,简单方便,而且不会漏。
如果Crouch层的动画有Timing加速播放属性,那么我们可以勾选Timing。将正常播放速度切换为当前动画的速度。在Base层放一个walk,然后Crouch层放一个fastwalk,其实就是将walk的速度加速。然后勾选timing,就可以达到加速效果。
里面的IK Pass属性,就是勾选取消或者选择当前动画机绑定的模型的所有IK动画的激活或者失活。比如说,IK动画绑定我们的手在翻墙的一瞬间是摁在墙上的,这就是一个IK动画绑定。以后会详细介绍。
9.Animation组件
首先,我们的Animator是通过播放Animation来完成的。那么Animation才是动画,Animator只不过是动画状态机,去播放Animation的。那么,我们现在来看看Animation面板的一些属性。
这是录制按钮,当我们点击录制按钮之后,我们对当前物体的任何改动都会记录到当前帧,如果没有则创建新的帧,如果当前存在一帧则会覆盖。
这里就是负责播放了,最左边表示跳转到动画的第一帧,最右边表示跳转到动画的最后一帧。然后就是上一帧和下一帧,最中间的是暂停。
框内数字表示你运行到多少帧了
表示你选择当前物体的动画
最右边有三个小点,表示设置。
那么首先是选择按秒来显示动画还是按帧数。
Ripple勾选上之后,我们移动关键帧的时候,会同时将左右两边边的帧同步移动,保持动画的连贯性。
出现的金色标识,标识左右两边的帧会进行同步移动。
然后就是ShowSampleRate就是显示当前的速率,
表示我们现在是一秒60帧,如果我们修改为120,那么就是一秒120帧,动画会变成两倍速。
同时我们也可以根据他给出的选项来设置
然后就是这个按钮
第一个表示FilterBySelection适合复杂关节的模型,比如说我在Cube下面创建一个Cube,那么我在子Cube点击这个Filter就会不显示父cube的动画帧只会显示作用于子物体的动画帧,可以让我们更好的设计
第二按钮表示添加帧
第三个表示添加动画事件,事件可以调用任何公共函数,所以可能性几乎是无限的,我们可以做任何事。
如果我们在选中Cube的情况下点击动画事件,那么只会让我们选择可以通过Cube调用的Public函数
如果我们在资源中找到这个动画,然后点击动画事件,那么我们就可以任意指定Public函数了。
然后就是曲线了
我们点击左边面板下面的Curves曲线,就会显示我们的动画帧以曲线的方式来显示。这样显示的好处便是更直观更方便修改。
右键之后,
ClampedAuto是Auto的升级版,Auto就是为了包含旧版本的功能而存在的。这是最基础的修改曲线的方法。我们可以移动那个关键节点
但是,当我们移动两边的杆子的时候,就会自动切换到FreeSmooth操作模式。也就是会平滑的为我们绘制曲线。
点击Flat就会变得不那么平滑,具体的情况根据实际需求来确定吧。
我们选择Broken之后就会切断两边的联系,单独进行操作。
然后就是让我们选择节点左右的线的情况,Free就是自由变化,Linear就是变成一条直线,
Constant变成一条水平的直线
Weighted让一边的比重变大。
10.利用Animation完成一个射击动画
首先,我们的fbx上的Animation如果设置为Humanoid属性之后,就没有FilterBySelection属性作用了。因为其动画都会被重新设计,以至于让所有人型模型都可以使用。
从Mixamo下载GunPlay动画
上面的是设置模型为humanoid之后的,下面这个是默认的动画。
那么,我们想要拿到进行修改,我们为自己的角色进行设计的话,最好还是用本来的动画。
所以我们在这里用原来的动画进行设计。
我们为其Hand的Transform下面添加一把枪,我们可以通过动画播放来调整其位置。
这样我们就有开枪动画了。
那么,我们就可以添加一个动画事件让其在开枪的一瞬间实例化一颗子弹
public void FireWeapon()
{
Rigidbody bulletInstance;
bulletInstance = Instantiate(bullet, spawnPoint.position, bullet.transform.rotation) as Rigidbody;
bulletInstance.AddForce(spawnPoint.forward * multiplier);
}
然后呢,我们就会发现射击的同时会发射子弹了。
但是,我们的枪会穿模,枪穿进人的脑袋里面去了。
因为我们的枪是后来加的,难免与动画设计师射击的枪械不一样,所以我们这个时候就可以通过Filter过滤器来进行修改我们的动画。
我们只需要将射击完的手臂往外拐一点点,就不会穿模了。
选择手臂,选择过滤器,那么我们就可以单独对手臂的动画帧进行设计。
这样就不会穿模了!
同时可以加大颈椎的旋转,增加感官冲击
11.RootMotion
根运动
角色在世界空间中的运动被“烘焙”
我们的动画其实是存在一点点偏移或者选择的,那么我们可以选择保留和删除。
我们可以在这里勾选是否ApplyRootMotion
我们点击动画,就会出现该面板。
LoopTime勾选即会循环播放动画
RootTransfromRotation和Position都是让我们选择是否去BakeIntoPose,我们选择Bake,就让它不产生影响。
这里是Fbx页面的Animation,标识绿色表Unity推荐你去BakeIntoPose。
我们在下面看到
当我们勾选了之后
发现我们的动画对坐标的影响就可以达到0了。