Unity中的动画系统和Timeline——笔记

前言:

最近开始跟着SIKI学院系统学习Unity,这篇文章就是Unity中的动画系统和Timeline的笔记

 

 

 

 

 

 

 


动画

动画的录制和动画曲线的编辑

以前我都是在动画中一步一步来做动画,从来不知道还有这个录制功能,厉害了

我们点击Animation编辑版中左上角的红点即可开始录制,录制状态中我们调整物体就可以自动创建帧。

另外我们还可以在Curves中编辑动画曲线,点击动画曲线的关键点我们还可以设置动画曲线变化的均匀程度

 

创建2D精灵动画

方法一:

  1. 直接将你的2D图片从Assets中多选住拖到场景中(它会自动创建一个同名的状态机并且一个拼接这些2D图片的短暂动画)
  2. 然后就可以在动画状态机中调整了。

 

方法二:

  1. 直接在Assets中创建动画状态机
  2. 把动画状态机给了场景中的2D物体,然后在该2D物体身上通过Ctrl+6创建动画
  3. 在动画中,通过sprite来编辑动画,可以直接将别的图片拖过来形成新的一帧。
  4. 然后就可以在动画状态机中调整了

 

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,就可以使用动画了。

我们来分别介绍一下GenericHumanoid

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

更多关于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区段时,该区段对应脚本则会每一帧地调用本方法。



 


商业转载 请联系作者获得授权,非商业转载 请标明出处,谢谢

 

 

 

2 thoughts on “Unity中的动画系统和Timeline——笔记

  • 头像
    2020年3月23日 at 上午11:35
    Permalink

    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!

    • 头像
      2020年3月23日 at 上午11:54
      Permalink

      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.

发表评论