Unity 生成自定义动画格式并播放

  • Unity 生成自定义动画格式并播放已关闭评论
  • 129 次浏览
  • A+
所属分类:.NET技术
摘要

原文链接:https://www.cnblogs.com/jingjiangtao/p/16666514.htmlUnity的AnimationClip.SetCurve()只在Editor中运行时有用,打包后运行时只对legacy的AnimationClip有用,对其它类型的动画Generic和Humanoid都不起作用。https://docs.unity3d.com/ScriptReference/AnimationClip.SetCurve.html。

原文链接:https://www.cnblogs.com/jingjiangtao/p/16666514.html

目的

Unity的AnimationClip.SetCurve()只在Editor中运行时有用,打包后运行时只对legacy的AnimationClip有用,对其它类型的动画Generic和Humanoid都不起作用。https://docs.unity3d.com/ScriptReference/AnimationClip.SetCurve.html

所以如果想在运行时加载和播放动画,只能用自定义格式。

注意:此自定义格式的生成、加载和播放,都不涉及重定向,通过特定的模型生成的动画,只能在这个模型上播放。

注意:本文只实现了人体模型的自定义动画,其它模型的实现思路相同。

思路

自定义动画格式的生成和播放有三个步骤:

  • 生成:把已经存在的动画片段转换成自定义动画格式
  • 序列化:保存和加载自定义动画格式
  • 播放:播放自定义动画格式

大致过程就是,以一定的帧率记录模型每个物体的position和rotation值,保存在自定义格式中,这种格式要能序列化。之后加载自定义格式,并以一定的帧率每帧设置物体的position和rotation,达到播放的效果。

第三方库

MessagePack

https://github.com/neuecc/MessagePack-CSharp

MessagePack是一种数据交换格式,可以生成体积更小的序列化文件,而且序列化和反序列化的速度更快。动画文件数据量比较大,生成的文件也比较大,所以最好选择生成文件体积更小的序列化方案。

实现

实体类

首先要定义实体类来保存动画数据。实体类的嵌套结构如下:

AnimDataSequence {     id: int,     length: float,     frameRate: float,     name: string,      animPathSequences: List<AnimPathSequence>     [         {             path: string,             localPosition: CurveVector3             {                 x: AnimationCurve,                 y: AnimationCurve,                 z: AnimationCurve             },             localRotation: CurveQuaternion             {                 x: AnimationCurve,                 y: AnimationCurve,                 z: AnimationCurve,                 w: AnimationCurve             }         },         ...     ] }

最外层是AnimDataSequence类,id字段保存动画id;length保存动画时长,单位秒;frameRate保存动画帧率;name保存动画名称。animPathSequences是一个数组,数组的元素类型是AnimPathSequence,保存着模型中单个子物体位置和旋转值的动画曲线,path表示这个子物体相对模型根节点的路径,所以List<AnimationPathSequence>保存了模型的所有物体位置和旋转值的动画曲线。

图示:

Unity 生成自定义动画格式并播放

 

 

每个实体类的代码如下:

AnimDataSequence.cs

[MessagePackObject] public class AnimDataSequence {     [Key(0)]     public int id;      [Key(1)]     public float length;      [Key(2)]     public float frameRate = 30f;      [Key(3)]     public string name;      [Key(4)]     public List<AnimPathSequence> animPathSequences = new List<AnimPathSequence>();      public List<AnimSequenceFrame> GetFrame(float time)     {         List<AnimSequenceFrame> sequenceFrames = new List<AnimSequenceFrame>(animPathSequences.Count);         foreach (AnimPathSequence sequence in animPathSequences)         {             AnimSequenceFrame frame = new AnimSequenceFrame();             frame.path = sequence.path;             frame.localPosition =                 new Vector3(                     sequence.localPosition.x.Evaluate(time),                     sequence.localPosition.y.Evaluate(time),                     sequence.localPosition.z.Evaluate(time));              frame.localRotation =                 new Quaternion(                     sequence.localRotation.x.Evaluate(time),                     sequence.localRotation.y.Evaluate(time),                     sequence.localRotation.z.Evaluate(time),                     sequence.localRotation.w.Evaluate(time));              sequenceFrames.Add(frame);         }          return sequenceFrames;     } }

GetFrame()函数获取给定时间位置的一帧数据,返回一个元素类型是AnimSequenceFrame的数组。

[Serializable] public class AnimSequenceFrame {     public string path;     public Vector3 localPosition;     public Quaternion localRotation; }

 

AnimSequenceFrame中,path表示这个子物体相对于模型根节点的路径,localPosition和localRotation表示这个物体在这一帧的位置和旋转值。

AnimPathSequence.cs

[MessagePackObject] public class AnimPathSequence {     [Key(0)]     public string path;      [Key(1)]     public CurveVector3 localPosition = new CurveVector3();      [Key(2)]     public CurveQuaternion localRotation = new CurveQuaternion(); }

CurveQuaternion.cs

[MessagePackObject] public class CurveQuaternion {     [Key(0)]     public AnimationCurve x;      [Key(1)]     public AnimationCurve y;      [Key(2)]     public AnimationCurve z;      [Key(3)]     public AnimationCurve w; }

CurveVector3.cs

[MessagePackObject] public class CurveVector3 {     [Key(0)]     public AnimationCurve x;      [Key(1)]     public AnimationCurve y;      [Key(2)]     public AnimationCurve z; }

AnimationCurve是Unity自有的类型,表示动画曲线,可以被MessagePack序列化和反序列化。其实,自定义的动画数据,最终都是保存在AnimationCurve中的。

生成

用Unity的Playable API获取动画指定时间的骨骼数据,保存到实体类对象中,最后保存成文件。

public void SaveAnimSequence(AnimationClip clip, Animator animator, string filePath, float frameRate = 0) {     // 创建PlayableGraph,用于播放动画     PlayableGraph playableGraph = PlayableGraph.Create("ConvertHumanoidAnimation");     playableGraph.SetTimeUpdateMode(DirectorUpdateMode.Manual);     // 把动画片段连接到PlayableGraph中     AnimationPlayableOutput animPlayableOutput = AnimationPlayableOutput.Create(playableGraph, "AnimationOutput", animator);     AnimationClipPlayable animClipPlayable = AnimationClipPlayable.Create(playableGraph, clip);     animPlayableOutput.SetSourcePlayable(animClipPlayable);      // 创建自定义动画对象,设置必要的参数     AnimDataSequence animSequence = new AnimDataSequence();     animSequence.length = clip.length;     animSequence.name = clip.name;     // 如果没有传帧率,就用原始动画片段的参数     animSequence.frameRate = frameRate == 0 ? clip.frameRate : frameRate;     // 计算每帧的持续时间     float frameDuration = 1f / animSequence.frameRate;      Transform root = animator.transform;     List<Transform> bones = new List<Transform>();      // 获取人体骨骼对应的物体Transform数组     for (int i = 0; i < (int)HumanBodyBones.LastBone; i++)     {         Transform boneTransform = animator.GetBoneTransform((HumanBodyBones)i);         if (boneTransform != null)         {             bones.Add(boneTransform);              AnimPathSequence pathSequence = new AnimPathSequence();             animSequence.animPathSequences.Add(pathSequence);              // 获取相对路径             pathSequence.path = Utils.RelativePath(root, boneTransform);              pathSequence.localPosition.x = new AnimationCurve();             pathSequence.localPosition.y = new AnimationCurve();             pathSequence.localPosition.z = new AnimationCurve();              pathSequence.localRotation.x = new AnimationCurve();             pathSequence.localRotation.y = new AnimationCurve();             pathSequence.localRotation.z = new AnimationCurve();             pathSequence.localRotation.w = new AnimationCurve();         }     }      // 开始获取动画对应的人体骨骼数据     float time = 0f;     while (time <= clip.length)     {         // 根据时间点定位动画         animClipPlayable.SetTime(time);         playableGraph.Evaluate();          for (int i = 0; i < bones.Count; i++)         {             Transform bone = bones[i];             AnimPathSequence pathSequence = animSequence.animPathSequences[i];              pathSequence.localPosition.x.AddKey(time, bone.localPosition.x);             pathSequence.localPosition.y.AddKey(time, bone.localPosition.y);             pathSequence.localPosition.z.AddKey(time, bone.localPosition.z);              pathSequence.localRotation.x.AddKey(time, bone.localRotation.x);             pathSequence.localRotation.y.AddKey(time, bone.localRotation.y);             pathSequence.localRotation.z.AddKey(time, bone.localRotation.z);             pathSequence.localRotation.w.AddKey(time, bone.localRotation.w);         }          // 下一帧的时间         time += frameDuration;     }      playableGraph.Destroy();      // 用MessagePack序列化并保存到文件     byte[] bytes = MessagePackSerializer.Serialize(animSequence);     File.WriteAllBytes(filePath, bytes); }

简单起见,代码最后直接把自定义动画数据保存到了文件。实际使用可以不保存文件,序列化后用于网络传输。

frameRate参数表示采样的帧率,值越大,文件体积越大,动画也越精确,需要取舍。

加载和播放

加载就是用MessagePack库把MessagePack数据反序列化;播放就是每帧播放指定时间点的动画曲线;

public class PoseSequencePlay : MonoBehaviour {     public bool IsPlaying => _isPlaying;      public float CurrentTime     {         get         {             return _time;         }          set         {             if (value > _animDataSequence.length) return;              _time = value;             SetPose();         }     }      public bool Loop { get; set; } = false;     public AnimDataSequence AnimDataSequence => _animDataSequence;      protected Transform _character;     protected AnimDataSequence _animDataSequence;     protected float _time = 0f;     protected bool _isPlaying = false;      protected virtual void Update()     {         if (!_isPlaying)         {             return;         }          if (_time > _animDataSequence.length)         {             if (Loop)             {                 Stop();                 Play();             }             else             {                 _isPlaying = false;             }              return;         }          SetPose();          // 增加时间         _time += Time.deltaTime;     }      protected void SetPose()     {         if (_animDataSequence == null || _character == null) return;         if (_time > _animDataSequence.length) return;          // 获取指定时间的序列数据         List<AnimSequenceFrame> frame = _animDataSequence.GetFrame(_time);         // 将获取到的数据赋值给对应的骨骼         foreach (AnimSequenceFrame frameOnePath in frame)         {             Transform boneTransform = _character.Find(frameOnePath.path);             if (boneTransform != null)             {                 boneTransform.localPosition = frameOnePath.localPosition;                 boneTransform.localRotation = frameOnePath.localRotation;             }             else             {                 Debug.LogWarning($"[GestureGenerator] {frameOnePath.path} is Null");             }         }     }      public virtual void PrepareData(string animDataSequenceFilePath, Transform character)     {         byte[] bytes = File.ReadAllBytes(animDataSequenceFilePath);         AnimDataSequence sequence = MessagePackSerializer.Deserialize<AnimDataSequence>(bytes);         PrepareData(sequence, character);     }      public virtual void PrepareData(byte[] sequenceBytes, Transform character)     {         AnimDataSequence sequence = MessagePackSerializer.Deserialize<AnimDataSequence>(sequenceBytes);         PrepareData(sequence, character);     }      public virtual async Task PrepareDataAsync(string animDataSequenceFilePath, Transform character)     {         AnimDataSequence sequence = null;         using (FileStream fileStream = File.OpenRead(animDataSequenceFilePath))         {             sequence = await MessagePackSerializer.DeserializeAsync<AnimDataSequence>(fileStream);         }          PrepareData(sequence, character);     }      public virtual void PrepareData(AnimDataSequence sequence, Transform character)     {         _isPlaying = false;         _time = 0f;         _animDataSequence = sequence;         _character = character;     }      public virtual void Play()     {         _isPlaying = true;     }      public virtual void Pause()     {         _isPlaying = false;     }      public virtual void Stop()     {         _isPlaying = false;         _time = 0f;         SetPose();     }      public virtual void Clear()     {         Stop();         _animDataSequence = null;         _character = null;     } }

PoseSequencePlay是一个继承了MonoBehaviour的类,需要挂载到Unity场景中运行。

加载的核心函数是PrepareData(),通过MessagePackSerializer.Deserialize()反序列化自定义动画数据。

播放的核心函数有两个:Update()和SetPose()。SetPose()函数根据当前时间将人物模型设置为自定义动画序列中的姿势,Update()函数每帧设置动画姿势,并递增时间。