前言:
最近开始跟着SIKI学院系统学习Unity,这篇文章就是Unity中的动画系统和Timeline的笔记
动画
动画的录制和动画曲线的编辑
以前我都是在动画中一步一步来做动画,从来不知道还有这个录制功能,厉害了
我们点击Animation编辑版中左上角的红点即可开始录制,录制状态中我们调整物体就可以自动创建帧。
另外我们还可以在Curves中编辑动画曲线,点击动画曲线的关键点我们还可以设置动画曲线变化的均匀程度
创建2D精灵动画
方法一:
- 直接将你的2D图片从Assets中多选住拖到场景中(它会自动创建一个同名的状态机并且一个拼接这些2D图片的短暂动画)
- 然后就可以在动画状态机中调整了。
方法二:
- 直接在Assets中创建动画状态机
- 把动画状态机给了场景中的2D物体,然后在该2D物体身上通过Ctrl+6创建动画
- 在动画中,通过sprite来编辑动画,可以直接将别的图片拖过来形成新的一帧。
- 然后就可以在动画状态机中调整了
3D模型
不同的建模软件会导出不同的模型,但是要记住.fbx是最支持Unity的格式。
一般我们的模型除了模型以外还会有材质和贴图,这两个东西就是模型的皮肤,非常重要。
导出的一般有两个方式:第一种是把动画和模型放在一起,第二种是将每一个动画导出成一个单独的文件(例如:xxxx@xxx.fbx 这种格式)
导入模型和解决材质、贴图丢失问题
首先要确保场景模型的材质球可以编辑,如果不能编辑怎么办?不要急,打开文件中的模型(是文件中,不是场景中!)然后在Inspector面板中的Materials下这样设置
use external materials(Legacy):使用外部材料(遗产)
use embeddedmaterials :使用嵌入材料
选好使用外部材料后,我们双击模型,找到他的网格渲染组件(Mesh Renderer)看看是否缺少材质球,如果不缺材质,那就是贴图的问题,在材质球编辑处添加适当的贴图即可。
指定好贴图后,我们可以改一下shader为标准,可以看得更真实,再加一下法线贴图也好。
指定好这些属性后及时你将场景中的模型删除了,文件中也已经指定好贴图了。
导入动画
Unity中导入的模型主要是由3DMAX、Maya等建模软件制作的,后缀为.fbx的文件。
点击文件后我们会发现关于动画导入的一些设置
Rig面板下:
我们的Animation type主要有如下选项:
Legacy:已经启用的导入方式(无法使用状态机)。
Humanoid:只能人形动画使用。
Generic:人形非人形都可以使用。
注意Humanoid和Generic使用状态动画机播放,不能使用Animation播放
后两者的区别:当有两个骨骼结构相同的模型时,其中一个有动画而另一个没有。就可以把两者都设置为Humanoid,没有动画的模型的Avatar Definition设为Copy From Other Avatar。赋值有动画模型的Avatar,就可以使用动画了。
我们来分别介绍一下Generic和Humanoid
Generic(通用的)
它是新的动画系统,支持非人形(怪物)动画,也支持人形动画,应用它会生成一套骨骼(Avatar)。
但它无法使用Humanoid动画重定向功能。即美术给一个模型做的动画,这些做的动画只能给这个模型使用,不能给其他模型使用。而Humanoid的动画重定向功能,可以实现一个模型的动画,给其他模型使用。
- Avatar Define:化身定义,或者说骨骼映射定义。
Create From This Model:使用这个Model创建骨架
Copy From Other Avatar:使用其他骨骼(前提是和另一个模型的骨骼相同)
- Root node:根节点
选择模型根节点
- Optimize(优化) Game Objects:是否优化游戏物体(在发布游戏时勾选)
Humanoid(人形的)
Humanoid最牛X的地方就是支持动画重定向
选择Generic或者Humanoid后,系统都自动为Perfab模型生成Avatar。这个Avatar可以提供给其他同Humanoid的骨骼用来共用Avator(动画重定向)
在我们将Avatar Definition选择CreateFromThisModel后,点击Apply后可以点击Configure来配置骨骼映射,会跳转到大概这样的场景
这里我们可以调整骨骼映射。
这里的白色,绿色骨骼都是我们的素材创造的骨骼,而Mapping(映射)里面为Unity自带骨骼,我们创建的骨骼要映射到Unity自带的骨骼上。
绿色、白色都是Unity内置骨骼,会跟人物的骨骼节点映射,白色为未映射正确的。
实线为必须映射骨骼,虚线为非必须的。
更改映射方法:点击Model里的白色骨骼,在Hierarchy里选择正确的骨骼节点,拖到它的Mapping(映射)对话框中
动画重定向:
Unity引擎中动画重定向的实现不是一个直观的方法,而是封装在了Humanoid类型的动画系统里面,也就是必须是人形的骨架、使用Humanoid才可以使用它。Unity没有像前文描述的基本原理那样去定义两套骨架之间的映射关系,而是自己在内部定义一套骨架模板,所有的Avatar骨骼都必须映射到这套模板上才可以由同一个Animator来驱动产生Retargeting之后的动画效果。比如我们给A创建了骨骼,并与Unity模板映射成功,给A的状态机配置了动画;现在我们来了个B人物,我们将它的骨骼配置一下与Unity骨骼模板基本绑定后,我们就可以让B去做出A动画状态机的动画(但是骨骼要填入各自的骨骼)
动画切割
在物体的Animation中可以进行动画的切割
按照提示直接切割就好。
下面的Loop Match 是用来检测动画片段的第一帧和最后一帧是否重合,方便我们做循环动画。
脚本处理
将动画名字符串转成哈希值方便调用
Animator.StringToHash("字符串");
它是Animator的静态方法,用Animator直接调用即可。
Animator.GetCurrentAnimatorStateInfo()
Animator.GetCurrentAnimatorStateInfo(a)
确定当前第a层动画的AnimatorStateInfo 对象(有关当前或下一个状态的动画器信息)。
属性:
- fullPathHash
该状态的完整路径哈希值。 - length
该状态的当前持续长度。 - loop
该状态是否循环。 - normalizedTime
该状态的归一化时间。 - shortNameHash
使用Animator.StringToHash生成的哈希值。传递的字符串不包含父层的名字。 - tagHash
该状态的标签
完整路径可以结合上面的StringToHash这样利用:
表示当前外层动画状态是否为外层的idle
补充:得到当前动画状态机播放的动画的方法:
//GetCurrentAnimatorStateInfo 获取动画控制器中指定层的状态信息
AnimatorStateInfo info = _anim.GetCurrentAnimatorStateInfo(0);
//现在播放的动画是第0层的normal
if ( Animator.StringToHash("normal").Equals(info.shortNameHash))
{
}
Animator.IsInTransition
bool IsInTransition(int layerIndex);
检测当前是否在过渡
layerIndex The layer’s index.
该层的索引。
0表示Base Layer
混合树:
一维混合树:
我们很多时候经常用一个变量来作为三个动画的转换条件,比如速度等于0则站着,大于0则走,再大点就跑起来,这样我们要做三个动画在后期是很不方便管理的,我们可以直接使用混合树来混合多个动画。
混合树也是一个state,但是可以混合多个动画,用一个值来做划分
我们在状态机中右击选择
生成新混合树后,双击就可以进入混合树编辑中,在混合树编辑中双击空白地方就可以退出混合树编辑。
如我设置的,就是根据一个参数speed,当它大于0,则向walk转换,大于0.5则开始向Run装换。
Automate Thresholds:自动设置阈值(我们可以取消勾选来亲自设置范围限制)
Adjust Time Scale:调整时间比例
这样,我们就做好了一个一维混合树,一个state就控制了站·走·跑的转换。
二维混合树:
二维混合树就是有了两个参数可以决定动画,我们常用的操作就是在站·走·跑的基础上加上左右旋转,如下图所示
PosY(即speedz参数)可以控制站·走·跑的播放,而PosX(即speedRotate参数)可以控制旋转动画的播放
我们可以Compute Position来自动计算阈值(根据动画分析阈值)
x的大小来自于动画的角速度(角度为单位)
在脚本中控制两个参数的设置即可。
private int speedRotateID = Animator.StringToHash("speedRotate");
private int speedZ = Animator.StringToHash("speedz");
void Update()
{
animator.SetFloat(speedZ, Input.GetAxis("Vertical")*4.1f);
animator.SetFloat(speedRotateID, Input.GetAxis("Horizontal") * 121f);
}
关于2D混合树有很多不同的类型,我们要选择恰当的类型
我来简单翻译一下:
2D Simple Directional(简单方向): 当你的动作代表不同的方向时,例如「向前走」、「向后走」、「向左走」、「向右走」、「向上走」、「向下看」、「向左看」及「向右看」 ,你就可以使用这些动作。 可以选择包括位置(0,0)的单个运动,如“空转”或“瞄准直线”。 在简单方向类型中,不应该有同一方向的多个动作,例如“向前走”和“向前跑”。
2D Freeform Directional(自由方向): 当你的动作代表不同的方向时,也可以使用这种混合类型,但是你可以在同一个方向上有多个动作,例如“向前走”和“向前跑”。 在自由方向类型的运动集应始终包括一个单一的运动在位置(0,0) ,如“空闲”。
2D Freeform Cartesian(自由的笛卡尔): 当你的动作不代表不同的方向时最好使用。 使用自由笛卡尔坐标系,你的 x 参数和 y 参数可以代表不同的概念,比如角速度和线速度。 例如“向前走不转弯”、“向前跑不转弯”、“向前走向右转弯”、“向前跑向右转弯”等动作。
Direct: 这种类型的混合树允许用户直接控制每个节点的权重。 有用的面部形状或随机闲置混合。
相机跟随
相机跟随的代码,自行参考
public class CameraControl : MonoBehaviour
{
private Transform player;
private Vector3 offset;
private float smoothing = 3;
// Start is called before the first frame update
void Start()
{
player = GameObject.FindGameObjectWithTag("Player").transform;
offset = transform.position - player.position;
}
// Update is called once per frame
void Update()
{
//player.TransformDirection(offset)将offset作为player的局部坐标再转换成世界坐标
//这样子,player的转向会对offset产生影响
Vector3 targetPosition = player.position+player.TransformDirection(offset);
transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * smoothing);
transform.LookAt(player.position);
}
}
MatchTarget目标匹配
我们有的动作会需要和场景中的东西互动(比如爬墙,跳跃翻墙),这个时候,为了保证真实性,我们需要就匹配目标
示例代码:
void Update()
{
animator.SetFloat(speedZ, Input.GetAxis("Vertical")*4.1f);
animator.SetFloat(speedRotateID, Input.GetAxis("Horizontal") * 121f);
bool isVault = false;
if (animator.GetFloat(speedZ) > 3&&animator.GetCurrentAnimatorStateInfo(0).IsName("locomotion")) {
RaycastHit hit;
//距离我们有墙,则跳跃
if (Physics.Raycast(transform.position + Vector3.up * 0.3f, transform.forward, out hit, 3.1f)) {
if (hit.collider.tag == "Obstacle") {
if (hit.distance > 2.9)
{
Vector3 point = hit.point;
point.y = hit.collider.transform.position.y + hit.collider.bounds.size.y+0.1f;
matchTarget = point;
isVault = true;
}
}
}
}
animator.SetBool(vaultID, isVault);
if (animator.GetCurrentAnimatorStateInfo(0).IsName("Vault")&&animator.IsInTransition(0)==false) {
animator.MatchTarget(matchTarget,Quaternion.identity,AvatarTarget.LeftHand,
new MatchTargetWeightMask(Vector3.one,0),0.25f,0.45f);
}
}
- animator.GetCurrentAnimatorStateInfo(0).IsName(“locomotion”):得到现在的第0层的动画状态,判断名字是否是 locomotion
- Physics.Raycast(transform.position + Vector3.up * 0.3f, transform.forward, out hit, 3.1f):从脚本挂载物体的位置往上提高0.3米的位置,向前方发出射线,返回射线碰撞信息给hit,射线的范围可达到3.1米
- hit.collider.bounds.size.y:射线碰撞体的限制范围(即大小)的尺寸的y值
- animator.GetCurrentAnimatorStateInfo(0).IsName(“Vault”):得到现在的第0层的动画状态,判断名字是否是 Vault
- animator.IsInTransition(0):第0层动画是否是在转换中
- animator.MatchTarget(matchTarget,Quaternion.identity,AvatarTarget.LeftHand,new MatchTargetWeightMask(Vector3.one,0),0.25f,0.45f):
- matchTarget:匹配的位置
- Quaternion.identity:旋转状态:无旋转
- AvatarTarget.LeftHand:匹配的人物位置:人物骨骼的左手
- new MatchTargetWeightMask(Vector3.one,0):一个权重掩码对象:位置权重为1(x:1,y:1,z:1),旋转权重为0。
- 0.25f,0.45f:匹配时间由动画的第0.25秒到0.45秒(开始匹配后会有差值运算)
曲线操作方法:
我们可以绑定某个状态机内的变量跟随动画的进行而改变。
在我们的资源中的animation里会有这么一栏
其中的Curves就是曲线的意思,我们给Curves取个名字,在相应动画状态机中的同名变量就会被绑定。
而这个曲线的波动就是随着动画的进行这个变量的取值。
动过下面的预览模式配合给曲线加关键帧就可以调整曲线。
IK动画与状态机分层
动画状态机可以进行分层来决定不同的动作
建立新层后,点击层右边的设置就可以来配置该层
- Weight是权重
- Mesh是骨骼遮罩
- Blending是混合方式:有覆盖——Override,以及添加——Additive
我们可以在Asset资源区新建一个骨骼遮罩——Avatar Mask,然后就是如下的画面
绿色就是可控制,红色就是不可控制,将骨骼遮罩赋值给动画层后,即可实现该层动画只对某部位的操作生效。
比如可以让手去拿起木头。
而实际操作中经常手和木头的位置不匹配,这个时候我们就可以借助反向动力学来解决这个问题。
IK是Inverse Kinematic的缩写,也就是反向动力学。
是根据骨骼的终节点来推算其他父节点的位置的一种方法。
比如通过手的位置推算手腕、胳膊肘的骨骼的位置。”
我们来看示例:
先运行该动画层IK Pass,并且骨骼遮罩允许需要的部分IK
然后我们可以创造空物体来把持位置,比如创造两个左手右手的空物体,移动到这个木头左右
然后就是代码控制
//每一帧都会调用
//每个勾选IK pass的层都会调用,可以通过参数判断哪一层调用了OnAnimatorIK
private void OnAnimatorIK(int layerIndex)
{
if (layerIndex == 1) {
//isHoldLogID为真则执行双手拿木头的动画
int weight = animator.GetBool(isHoldLogID) ? 1 : 0;
//左手位置以及旋转跟随lefthand的位置和旋转,weight控制权值
animator.SetIKPosition(AvatarIKGoal.LeftHand,lefthand.position);
animator.SetIKRotation(AvatarIKGoal.LeftHand, lefthand.rotation);
animator.SetIKPositionWeight(AvatarIKGoal.LeftHand, weight);
animator.SetIKRotationWeight(AvatarIKGoal.LeftHand, weight);
//右手位置以及旋转跟随righthand的位置和旋转,weight控制权值
animator.SetIKPosition(AvatarIKGoal.RightHand, righthand.position);
animator.SetIKRotation(AvatarIKGoal.RightHand, righthand.rotation);
animator.SetIKPositionWeight(AvatarIKGoal.RightHand, weight);
animator.SetIKRotationWeight(AvatarIKGoal.RightHand, weight);
}
}
这样一来,我们就可以自由固定双手的位置了
Timeline
介绍
这一个技术相对于其他动画系统,最大的区别就是,TimeLine针对多个游戏物体做出的一系列动画,主要用于过场动画的制作,实现电影级的那种分镜效果
这一技术很牛逼哦!
我们可以通过菜单栏中的 Window-》Sequencing-》Timeline,打开 timeline 编辑器
然后就可以选择某游戏物体来创建一个Timeline。
在一个timeline中,我们可以通过拖进来物体来创建轨道,然后就可以随心编辑轨道了。
利用Timeline可以轻松制作很多炫酷的分镜头
自定义脚本
我们还可以实现自定义扩展
在Asset资源区中找个合适的目录创建一个适用于Timeline的脚本
然后创建第二个脚本——Playable Asset ,示例代码如下:
[System.Serializable] //序列化,让外边能显示
public class TestAssert : PlayableAsset
{
[Header("对话框")]
public ExposedReference<Text> dialog;
[Multiline(3)]
public string dialogStr;
private Test test = new Test();
// Factory method that generates a playable based on this asset
public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
{
test.dialog = dialog;
test.dialogStr = dialogStr;
Playable playable = ScriptPlayable<Test>.Create(graph,test);
return playable;
}
}
其中:ExposedReference:是一个泛型类型,可用于创建对场景对象的引用,以及通过使用上下文对象在运行时解析它们的实际值。ScriptableObject 或 PlayableAsset 等资源可使用它来创建对场景对象的引用。如果不使用ExposeReference,只是Text等类型,无法从那个Unity面板得到引用。
然后创建第一个脚本——Playable Behavior,示例代码如下:
public class Test : PlayableBehaviour
{
public ExposedReference<Text> dialog;
private Text _dialog;
public string dialogStr;
// Called when the owning graph starts playing
public override void OnGraphStart(Playable playable)
{
//Resole:根据 ExposedPropertyResolver 上下文对象,通过解析此引用的值来获取该值。
_dialog = dialog.Resolve(playable.GetGraph().GetResolver());
}
// Called when the state of the playable is set to Play
public override void OnBehaviourPlay(Playable playable, FrameData info)
{
//文本框显示文字
_dialog.gameObject.SetActive(true);
_dialog.text = dialogStr;
}
// Called when the state of the playable is set to Paused
public override void OnBehaviourPause(Playable playable, FrameData info)
{
if (_dialog)
{
//播放暂停时关闭文字的显示
_dialog.gameObject.SetActive(false);
}
}
}
设置好之后,我们将Playable Asset拖拽到timeline中,然后设置时间段上的参数,即可实现对应效果。
示例:
总结一下:
Playable Behavior 是实现自定义功能的主要脚本。
Playable Asset是沟通Playable Behavior和Unity的timeline面板的桥梁。
另外,在Playable Behavior中的几个方法的调用时机分别是:
- OnGraphStart:当该PlayableBehaviour的PlayableGraph启动时调用,以上面gif中的情况为例,timeline一开始,就会调用四次该方法,不管有没有进入TestAsset区段,timeline一开始就调用了该方法。如果我们中间暂停了,然后又开始,那么还会调用四次该方法。
- OnGraphStop:该函数在PlayableBehaviour片段停止播放时调用,以上面gif中的情况为例,timeline一结束/暂停,就会调用该方法,如果上面动画播完,就会瞬间调用四次该方法,如果中间动画被暂停,也会调用四次。
- OnBehaviourPlay:当该PlayableBehaviour的PlayState转换为PlayState.Play时调用,以上面gif中的情况为例,timeline运行进入某个TestAsset区段时,该区段就会调用本方法一次。另外,在某区段中启动timeline,则会调用一次该区段负责的脚本的本方法,如果是空白区段则不会调用。
- OnBehaviourPause:该函数在PlayableBehaviour片段的PlayState转换为Pause时调用,以上面gif中的情况为例,timeline运行离开某个TestAsset区段时,该区段就会调用本方法一次,注意timeline一开始启动会调用n次来暂停n个区段,例如上图,timeline一启动就会调用四次。另外,在某区段中暂停timeline,则会调用一次该区段负责的脚本的本方法,如果是空白区段则不会调用。
- PrepareFrame:在该PlayableBehaviour播放的每一帧中调用,以上面gif中的情况为例,timeline运行进入某个TestAsset区段时,该区段对应脚本则会每一帧地调用本方法。
商业转载 请联系作者获得授权,非商业转载 请标明出处,谢谢
Long time supporter, and thought I’d drop a comment.
Your wordpress site is very sleek – hope you don’t mind me asking what theme you’re using?
(and don’t mind if I steal it? :P)
I just launched my site –also built in wordpress like yours– but the
theme slows (!) the site down quite a bit.
In case you have a minute, you can find it by searching for “royal cbd” on Google (would appreciate any feedback) – it’s still in the works.
Keep up the good work– and hope you all take care of yourself during the coronavirus scare!
My theme is called “radiate”. You should find it in WordPress theme. Good luck!
Thank you for your support. Now the whole world is plagued by viruses, I hope you also pay attention to your health.