目的
Unity的AnimationClip.SetCurve()只在Editor中运行时有用,打包后运行时只对legacy的AnimationClip有用,对其它类型的动画Generic和Humanoid都不起作用。https://docs.unity3d.com/ScriptReference/AnimationClip.SetCurve.html。
所以如果想在运行时加载和播放动画,只能用自定义格式。
注意:此自定义格式的生成、加载和播放,都不涉及重定向,通过特定的模型生成的动画,只能在这个模型上播放。
注意:本文只实现了人体模型的自定义动画,其它模型的实现思路相同。
思路
自定义动画格式的生成和播放有三个步骤:
- 生成:把已经存在的动画片段转换成自定义动画格式
- 序列化:保存和加载自定义动画格式
- 播放:播放自定义动画格式
大致过程就是,以一定的帧率记录模型每个物体的position和rotation值,保存在自定义格式中,这种格式要能序列化。之后加载自定义格式,并以一定的帧率每帧设置物体的position和rotation,达到播放的效果。
第三方库
MessagePack
https://github.com/MessagePack-CSharp/MessagePack-CSharp
MessagePack是一种数据交换格式,可以生成体积更小的序列化文件,而且序列化和反序列化的速度更快。动画文件数据量比较大,生成的文件也比较大,所以最好选择生成文件体积更小的序列化方案。
下文的序列化代码只针对JIT编译(Mono),如果需要IL2CPP编译,则MessagePack的序列化方法需要先做AOT,具体方法见官方文档:https://github.com/MessagePack-CSharp/MessagePack-CSharp#aot-code-generation-support-for-unityxamarin
效果
实现效果如下,其实也播放普通动画并无区别,GIF图片帧率有限,实际帧率和Unity有关:
实现
实体类
首先要定义实体类来保存动画数据。实体类的嵌套结构如下:
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>保存了模型的所有物体位置和旋转值的动画曲线。
图示:
每个实体类的代码如下:
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);
}
public static class Utils
{
public static string RelativePath(Transform root, Transform target)
{
string path = target.name;
try
{
while (target.parent != root)
{
target = target.parent;
path = target.name + "/" + path;
}
}
catch
{
path = string.Empty;
}
return path;
}
}
简单起见,代码最后直接把自定义动画数据保存到了文件。实际使用可以不保存文件,序列化后用于网络传输。
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()函数每帧设置动画姿势,并递增时间。
如有错误,欢迎指正,谢谢!