Unity AnimationClip详解(1)

news2024/9/22 5:36:01

【动画片段】

前文我们介绍了骨骼动画,在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博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2120514.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

通信工程学习:什么是GSMP通用交换机管理协议

GSMP:通用交换机管理协议 GSMP(General Switch Management Protocol,通用交换机管理协议)是一种用于IP交换机对ATM交换机进行控制的协议。以下是对GSMP的详细解释: 一、定义与概述 GSMP是一种异步协议,它在…

Python画笔案例-040 绘制五角星顶圆

1、绘制五角星顶圆 通过 python 的turtle 库绘制五角星顶圆,如下图: 2、实现代码 绘制五角星顶圆,以下为实现代码: """五角星顶圆.py """ import turtledef draw_circle(d):turtle.left(90)for _ …

C#/.NET/.NET Core技术前沿周刊 | 第 4 期(2024年9.1-9.8)

前言 C#/.NET/.NET Core技术前沿周刊,你的每周技术指南针!记录、追踪C#/.NET/.NET Core领域、生态的每周最新、最实用、最有价值的技术文章、社区动态、优质项目和学习资源等。让你时刻站在技术前沿,助力技术成长与视野拓宽。 欢迎投稿&…

客户端绑定本地端口与服务器建立连接的详细实现

客户端绑定本地端口与服务器建立连接的详细实现 一、网络编程基础1.1 TCP/IP协议1.2 套接字(Socket)1.3 客户端与服务器模型二、客户端程序的设计2.1 需求分析2.2 流程设计三、具体代码实现3.1 伪代码3.2 C代码实现四、代码详解4.1 初始化套接字库4.2 创建套接字4.3 绑定本地…

嵌入式Linux学习笔记-C语言编译过程

c语言的编译分为4个过程,分别是预处理,编译,汇编,链接。 一、预处理 预处理是c语言编译的第一个阶段,该任务主要由预处理器完成。预处理器会根据预处理指令对源代码进行处理,将预处理指令替换为相应的内容…

Learn ComputeShader 12 Setting up a buffer-based particle effect

unity有自己的粒子系统,但是这次我们要尝试创建一个我们自己的粒子系统,而且使用计算着色器有下面这些好处。总而言之,计算着色器适合处理大规模的数据集。例如,能够高效地处理数万个甚至数百万个粒子的计算。这对于粒子系统这样的…

【HarmonyOS】骨架屏(数据请求回来之前使用组件生成骨架屏,数据回来以后【骨架屏】消失)不会让页面白屏显示空

#骨架屏作用用途 骨架屏用途就是防止用户焦虑(为了迷惑用户) #效果图 #思路: #步骤: 1.首先是封装一个骨架 (所使用的思路就是利用 linearGradient translate animation onAppear 实现骨架的闪光效果&#xff09…

【计算机网络】电路交换、报文交换和分组交换——三种交换方式性能分析

【计算机网络】电路交换、报文交换和分组交换——三种交换方式性能分析 目录 【计算机网络】电路交换、报文交换和分组交换——三种交换方式性能分析电路交换性能分析报文交换性能分析分组交换性能分析对比 电路交换性能分析 4Kb 需要8毫秒 数据传送需要11ms 报文交换性能分…

数组与贪心算法——452、435、646、406、169(1简4中)

452. 用最少数量的箭引爆气球(中等) 有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。 一支弓箭可…

Python自带日志库实现springboot彩色效果

整体目标 涉及的库均为Python3自带库实现 loggingsysenum 终端显示彩色基本原理参考👉Terminal里的颜色的那些事 Python打印日志可以直接借用logging自带的库实现,但是默认的打印实在太丑了,长下面这样 这只是一条日志看着还好比较清爽&…

VUE+Tailwind网页开发

从nodejs官网下载安装包并安装&#xff1a;https://nodejs.org/zh-cn 参考vue官网步骤配置项目&#xff1a;https://cn.vuejs.org/guide/quick-start.html $ npm create vuelatest $ cd <your-project-name> $ npm install 参考&#xff0c;安装vue-router:安装 | Vue…

【CanMV K230】矩形检测

【CanMV K230】矩形检测 什么是 矩形检测矩形检测应用领域1.目标检测2.自动驾驶3.医学图像处理4.智能零售5.图像识别6.计算机视觉 K230应用相关函数官方例程 本篇内容&#xff1a; 什么是 矩形检测矩形检测应用领域K230应用&#xff08;包含相应函数及例程&#xff09; B站视频…

【有啥问啥】HashHop在LTM-2-mini中的应用:解锁长期记忆模型的新纪元

HashHop在LTM-2-mini中的应用&#xff1a;解锁长期记忆模型的新纪元 引言 随着AI技术的飞速发展&#xff0c;模型在处理复杂任务和数据时所需的上下文窗口大小也在不断扩展。深度学习模型在处理超长上下文时&#xff0c;往往面临着计算资源消耗高、上下文丢失等问题。近期&am…

通信工程学习:什么是IFMP(Ipsilon流管理协议)

IFMP&#xff1a;Ipsilon流管理协议 IFMP&#xff08;Ipsilon Flow Management Protocol&#xff09;&#xff0c;即Ipsilon流量管理协议&#xff0c;是一种用于网络流量管理的协议。它主要用于IP交换机、IP交换网关或IP主机中&#xff0c;通过控制数据传送&#xff0c;将现有网…

【H2O2|全栈】更多关于HTML(1)HTML进阶(一)

目录 HTML进阶知识 前言 准备工作 标签的扩展&#xff08;一&#xff09; 本文中的标签在什么位置使用&#xff1f; title标签 meta标签 name viewport referrer http-equiv charset content link标签 实际案例 可视部分 代码分析 其他标签 base标签 styl…

《论企业集成平台的技术与应用》写作框架,软考高级系统架构设计师

论文真题 企业集成平台是一个支持复杂信息环境下信息系统开发、集成和协同运行的软件支撑环境。它基于各种企业经营业务的信息特征,在异构分布环境(操作系统、网络、数据库)下为应用提供一致的信息访问和交互手段,对其上运行的应用进行管理,为应用提供服务,并支持企业信…

数论技巧——使用线性筛法去求1~n之间欧拉函数的和

本节是数论中的重要内容&#xff0c;也是算法竞赛中的常考点&#xff0c;初学者理解起来可能有些困难&#xff0c;需要多多体会 给定一个正整数 n&#xff0c;求 1∼n 中每个数的欧拉函数之和。 欧拉函数的定义&#xff1a;1~n中与n互质的数的个数被称为欧拉函数,记作φ(n) 欧…

操作系统 ---- 进程的概念、组成、特征

学习路线&#xff1a; 一、进程的概念及组成 我们通过一个例子来说明进程的概念以及程序和进程的区别。 我们在Windows操作系统中打开任务管理器&#xff0c;在任务管理器当中能看到此时系统当中运行的进程有哪些&#xff0c;如下图所示&#xff1a; 此时&#…

【前端】vue+html+js 实现table表格展示,以及分页按钮添加

一. 问题描述 数据条数太多显示到页面上时可能会渲染较慢&#xff0c;因此需要截取数据进行展示。 二. 代码写法 思路&#xff1a;按照上述图示思路&#xff0c;需要有两个数据列表&#xff0c;一个存储的是所有的列表数据&#xff0c;一个存储的是展示的数据列表&#xff0c…

蒙特卡罗——三门问题python代码实现

三门问题 b站李永乐老师讲解三门问题 python蒙特卡罗模拟 #模拟三门问题 import random as rd #n:模拟次数,m:中奖次数 n100000 m0 for i in range(n):#车位于的门号carrd.randint(0,2)#人随机选择一个门doorrd.randint(0,2)#主持人展示空门empties{0,1,2}-{car,door}emptyrd…