Unity AnimationClip详解(1)

【动画片段】

前文我们介绍了骨骼动画,在Unity中骨骼动画的部分静态数据存储在SkinedMeshRender中,而另一部分动态的关键帧数据就是存储在AnimationClip中的。

关键帧数据来自与FBX、OBJ等动画模型文件,可以在动画导入后的Animation选项卡中查看动画,Unity将其分为了四个区域,可以在动画预览区域,播放动画和查看特定帧的动画。

(A区和B区是在运行时并不会用到,C区严格来说,属于动作系统的一部分和动画片段没关系,但因为和动画系统联系紧密,会和AnimationClip关联起来,在后文中再详细说说)

预览动画所需的数据就在AniamtionClip中,图中所示的即为AniamtionClip文件:

可以双击来查看具体的动画数据,如下所示:

由于这里的AniamtionClip数据来自动画模型文件,所以是不可以修改的。在win的资源管理器中,也看不到这个文件,因为在Unity工程中看到的是Object而不是Asset。(Object与Asset的区别

可以将AnimationClip的数据Copy一遍,生成单独的文件后,即可编辑。Unity提供了交互的方式,但可以通过代码自动生成,例如:

    public void CopyAnimationClip(GameObject go)
    {
        AnimationClip[] clips = AnimationUtility.GetAnimationClips(go);
        foreach (AnimationClip clip in clips)
        {
            AnimationClip newClip = new AnimationClip();
            newClip.name = clip.name + "_auto";
            newClip.frameRate = clip.frameRate;
            newClip.legacy = clip.legacy;
            var setting = AnimationUtility.GetAnimationClipSettings(clip);
            AnimationUtility.SetAnimationClipSettings(newClip, setting);
            EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(clip);//https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/Animation/EditorCurveBinding.bindings.cs
            foreach (var binding in bindings)
            {
                AnimationCurve curve = AnimationUtility.GetEditorCurve(clip, binding);
                AnimationUtility.SetEditorCurve(newClip, binding, curve);
            }
            AssetDatabase.CreateAsset(newClip, "Assets/" + newClip.name + ".anim");
            AssetDatabase.SaveAssets();
          
        }
        AssetDatabase.Refresh();
    }

随后,我们就可以找到该文件,可以发现其实质是个YAML文本文件,其中存放了关键帧的数据

【关键帧数据】

一般UI动画为30帧,局内动画为60帧,前文列举的骨骼层级结构中有30个骨骼,每个骨骼至多有10个通道的数据,分为是S缩放、Q旋转(一般用四元数表示)、T位移。

因此,在数据结构设置上,AnimationClip需要是这样的:

   /// Class AnimationClip
   /// {
   ///     Bone* boneData //需要持有每个骨骼,这里不一定是要引用或者指针,每个骨骼可以有一个ID标识符,看游戏引擎如何计算了,通过标识符索引骨骼数据也可以。长度骨骼数,这里是30
   /// }

    /// Class Bone
    /// {
    ///     string bonename
    ///     int boneid
    ///     Channel* channelData //需要持有每个通道的数据,这里不一定是要引用或者指针,每个通道可以有一个ID标识符,看游戏引擎如何计算了,通过标识符索引通道数据也可以。长度至多是10
    /// }

    /// Class Channel
    /// {
    ///     string channelname
    ///     int channelid
    ///     float* data //30帧有31个数据,60帧有61个数据,这里就是真正的动画数据
    /// }

(为了提高性能,降低内存,这些在引擎底层一般会用struct而不是class)

以时间为X轴,以动画数据中某个骨骼的某个通道的值为Y轴,得到的是一系列孤立的点,点与点之间的数据通常通过插值得到。

通常都会用线性插值,也即我们可以在XY图中,直接将两点依次用直线连接起来,得到折线图。

折线图的变化陡峭(看点的切线变化,基本的数学知识),如果角色前后两帧的变化不大那么直接用折线图没问题,否则我们会看到角色动作不连贯,不流畅。

(注意,数学知识针对的是两个点,不是前后两帧的动作,可能前后两帧动作变化很小,但对动作中某些骨骼的某些通道的值而言变化很大;还有可能前后两帧动作变化很大,但对动作中某些骨骼的某些通道而言变化很小)

为解决动作不流畅的问题,我们需要用曲线去拟合这些点。

通常情况下,我们了解到的曲线拟合,大多都是用一条能够用一段函数描述的曲线去拟合不同点的分布。

这里的拟合要求曲线必须经过这些点,因此,我们需要用一个多段函数来描述曲线。

每段用什么函数可以是任意的,但考虑到对性能等的要求,在游戏中基本都用贝塞尔曲线或三次多项式。

【贝塞尔曲线】

原理

基本原理见链接:https://juejin.cn/post/7082701281969569829

更详细的见视频:https://www.youtube.com/watch?v=aVwxzDHniEw

应用

基本上三阶贝塞尔曲线就够用了,unity中各类曲线的编辑、游戏中道路、水管等的建设、移动轨迹、配置数据等都可以用到贝塞尔曲线

拓展

可以看到,几个点即可描述一条贝塞尔曲线。描述二阶贝塞尔曲线,需要三个点;描述三阶贝塞尔曲线,需要四个点。

如果只有两个已知点,怎么构造二阶贝塞尔曲线。

必须借助这两个点和其他默认数据算出默认的第二个点:(已知P1 P3,需要得到P2)例如:

  1. 第二个点和这两个点构成等边三角形
  2. 第二个点在这两个点连线的中间,和连线的距离是连线的长度(即构成等腰三角形)
  3. 第二个点在这两个点连线的中间,和连线的距离是连线的长度* t 。对于不同组的两个点,t可以始终是一个默认的值;也可以是根据其他因素算出来的一个值。这里额外引入了一个参数
  4. 第二个点和两个点的水平距离通过参数t1控制,和连线的距离通过参数t2控制。对于不同组的两个点,t1、t2可以始终是一个默认的值;也可以是根据其他因素算出来的一个值。这里又额外引入了一个参数
  5. 上述方式不能控制曲线的倾斜,需要通过斜率来控制。引入两个参数t1、t2,分别表示两个点的斜率,斜率的连线交点为第三个点。对于不同组的两个点,t1、t2可以始终是一个默认的值;也可以是根据其他因素算出来的一个值,获取从某个地方读取/获取的值。

按照上述方式,只有三个已知点,也可以构造出三阶贝塞尔曲线。

如果只有两个已知点,怎么构造三阶贝塞尔曲线。可想而知,需要有更多的参数来计算出其他两个点:

  1. P2、P3和已知的两个点构成正方形
  2. P2、P3和已知的两个点构成长放形,其宽为长度*t
  3. P2、P3和已知的两个点构成等腰梯形,其高为长度*t1,另一个底为长度*t2
  4. P2、P3和已知的两个点构成梯形,其高为长度*t1,P2和P1的距离为长度*w1,P3和P4的距离为长度*w2。这里又引入了额外的w1和w2两个权重参数
  5. 在4的基础上引入斜率参数t1和t2以替代高
  6. P2、P3和已知的两个点构成四边形,在5的基础上,对x和y采用相同的权重
  7. 在6的基础上,对y采用不同的权重,再额外引入两个参数w3、w4

【动画曲线及API】

AnimationCurve

AnimationClip中最为重要和核心的数据是AnimationCurve,对应上文说的Bone。我们可以通过GetCurve和SetCurve来从AnimationClip中获取和设置曲线,这个过程就像是从Dictionary中Get和Set一样。

(Add和Delete有时候可以合并到Set中,Set时Key是新的,表示Add;Set是Data是空的,表示Delete)

Get和Set操作都需要Key和Data,此时的Key就是每个骨骼,也即每个的名字,考虑到名字会有重复的,会用相对于根节点的路径来表示,通过相对路径可以找到每个节点;Data就是AnimationCurve了。

一个节点至少有10个Channel的数据,因此,还需要第二个Key来表示哪个Channel,因此GetCurve和SetCurve方法至少要是这样的:

AnimationClip.SetCurve(string relativePath,string channel,AnimationCurve data)

AnimationCurve中最核心和重要的数据是KeyFrame,对应上文说的Channel。同样的,AnimationCurve有对KeyFrame增删改查的接口。

两个KeyFrame做曲线拟合时用的是二阶或三阶贝塞尔曲线,有上文的拓展就可以轻松看懂KeyFrame的给个字段的含义和作用了。

注意,上文说的只是两个点之间做曲线拟合时的情况,实际上两个点之间的路径多种多样,可能保持不变的(Constant)、也可能是一条直线(Constant)、也可能是曲线,这就是KeyFrame的TangentMode。

在Unity中,可以通过AnimationUtility.Get/SetKeyLeftTangentMode系列接口给KeyFrame设置TangentMode。

更多曲线

当然,曲线不仅仅是Unity中可用的这几种,DoTween中的Ease曲线给我们展示了更多的曲线

而数学上的各类曲线会更多,只不过在游戏中有些用的很少罢了。

如果引入更多的曲线,我们可以给KeyFrame再添加一个CurveMode字段。因为不同的曲线所需的参数有差别,我们势必给KeyFrame添加新更多的新的参数,也即KeyFrame中必须包含所有CurveMode的所需参数的所有字段。

这是常见的解决方式,我们在面向对象编程中也经常如此,例如,在某个类中引入一个字段只为了解决某个特殊情况,大多数情况下却不需要此字段;如果多数情况下需要,在面向对象中可用继承,只在特定子类中有该字段。而这里并不是对象,也不能做成对象,因此只能包含所有的方式。

随着CurveMode越多,这必然导致很多字段是空着的,在数量极为庞大时占用很多内存却没有作用。解决该问题,就需要针对每种Curve的特点,想办法共享或合并参数,从而导致复杂度上升,也即后来者的理解成本变大了。

更长动画曲线

两个点之间的曲线是路径曲线,一系列点组成的曲线叫动画曲线AnimationCurve。他们是不同层级的对象,AnimationCurve除了KeyFrame的核心数据外,还有自己的其他数据。例如:

  • 长度——多长时间的动画
  •  WrapMode:——时间是有限的,动画长度是有限的,当前时间超过动画长度时表现是怎么样的,一般为:
    • Once——仅一次,超出时间都取0
    • Loop——循环,超出时间从开始再计
    • PingPong——来回
    • Clamp
  • 最大值
  • 最小值
  • 极值
  • 高度:最大值与最小值之差
  • 等等

曲线操作

我们可以对某个已知的动画曲线本身做一些特殊操作,例如:

  • 增加一些新的点数据
  • 删掉一些新的点数据
  • 改变曲线的高度或长度
  • 修改某些点的数据
  • 拉伸拉高或压缩压低曲线
  • 镜像反转曲线
  • 分割成多个子动画曲线

还可以对动画曲线之间做操作,例如:

  • 将两个动画曲线拼接起来合成一个新的动画曲线
  • 从一个或多个动画曲线中截取一部分拼接成新的动画曲线

这些操作都可以在代码中实现,可能要引入一些新的参数并给这些参数一些默认的值。

从代码架构上来看,我们会将这些操作作为静态方法放在AnimationCurve类中

如果某些操作不会在运行时用到,我们会将这些静态方法放在一个Utility类中以便做代码裁剪。

这些操作更多的其实更多的不是从代码上自动做的,而是要提供交互界面给人编辑的。人工编辑时相当于给参数赋值了。

编辑曲线

在Unity中,可以手动编辑动画曲线。一般来说,从动画文件中获取得到的动画曲线是不允许编辑的。

那么我们编辑动画曲线用于什么样的场景呢?这就涉及到动画曲线的本质了。

其本质就是一个值随时间变化的曲线,这个值不一定是动画中的,可以是其他任意的。

在游戏场景中,其可以是Cube的位置随时间变化的曲线。

因此,在编辑时,我们需要指定对象并指定对象中的哪个属性与曲线关联,对象可以是我们自定义的某个类,属性可以是这个类中的某个字段。

所以,AnimationClip的Get/SetCurve方法,可以变成这样:

AnimationClip.SetCurve(string relativePath,Type type,string propertyName,AnimationCurve Data)

在Unity的动画中,常用的Type是Transform,propertyName就是Transform的字段了。

拓展

AnimationCurve用于编辑时,只能对简单的对象做些简单运动,如果对象繁多且变化复杂,那么其会存在性能、内存、编辑效率等问题,在实际的工程中应用不多

但是其原理时可以扩展的,如果有一个固定的时间轴的,我们可以选择任意多个物体,及物体上任意的MonoBehaviour的属性,那么就可以编辑很多动画而不是在代码中取实现了。这就是Timeline的作用。

【参考】

Unity动画关键帧插值_unity inweight outweight-CSDN博客

  • 4
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值